EnsembleUI / ensemble

Build native apps 20x faster than Flutter, RN or any other tech
https://ensembleui.com/
BSD 3-Clause "New" or "Revised" License
123 stars 15 forks source link

Show UI for FlutterError and async errors #570

Open vusters opened 1 year ago

vusters commented 1 year ago

Looking at main.dart, we handle errors on the widget tree (case 1), but simply prints out the error for (2), and (3).

void initErrorHandler() {
  // case 1 - widget errors
  ErrorWidget.builder = (FlutterErrorDetails errorDetails) {
    return ErrorScreen(errorDetails);
  };

  /// case 2 - Flutter widgets and internal code
  FlutterError.onError = (details) {
    if (details.exception is EnsembleError) {
      debugPrint(details.exception.toString());
    } else {
      debugPrint(details.exception.toString());
    }
  };

  // case 3 - async Dart error
  PlatformDispatcher.instance.onError = (error, stack) {
    debugPrint("Async Error: " + error.toString());
    return true;
  };
}

We should display same ErrorScreen for #2 and #3.

vinothvino42 commented 1 year ago

@vusters Can I have something like this below with an Overlay? I just used similar to the ErrorScreen widget.

Here, there's one problem onError is called in every situation (for ex: assets not found). Is it okay? Also, the user needs to reload the view after fixing the issue, we couldn't find whether the user fixed the issue or not. So they need to refresh the whole app (like hot restart)

FlutterError.onError = (details) {
    print('onError else called');
    debugPrint(details.exception.toString());
    final state = errorKey.currentState;
    if (state != null && !state.isOverlay) {
      state.createErrorOverlay(FlutterErrorDetails(exception: details));
    }
  };

  // async error
  PlatformDispatcher.instance.onError = (error, stack) {
    debugPrint("Async Error: " + error.toString());
    final state = errorKey.currentState;
    if (state != null && !state.isOverlay) {
      state.createErrorOverlay(FlutterErrorDetails(exception: error));
    }

    return true;
  };

class AppHandler extends StatefulWidget {
  const AppHandler({super.key, required this.child});

  final Widget child;

  @override
  State<AppHandler> createState() => AppHandlerState();
}

class AppHandlerState extends State<AppHandler> {
  OverlayEntry? overlayEntry;
  bool isOverlay = false;

  void createErrorOverlay(FlutterErrorDetails? errorDetails) {
    // Remove the existing OverlayEntry.
    removeErrorOverlay();
    if (errorDetails == null) {
      removeErrorOverlay();
      return;
    }

    assert(overlayEntry == null);

    List<Widget> children = [];

    // main error and graphics
    children.add(Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Text(Utils.randomize(["Oh Snap", "Uh Oh ..", "Foo Bar"]),
            style: const TextStyle(
                fontSize: 28,
                color: Color(0xFFF7535A),
                fontWeight: FontWeight.w500)),
        const Image(
            image: AssetImage("assets/images/error.png", package: 'ensemble'),
            width: 200),
        const SizedBox(height: 16),
        // Text(
        //   widget.errorText +
        //       (widget.recovery != null ? '\n${widget.recovery}' : ''),
        //   textAlign: TextAlign.center,
        //   style: const TextStyle(fontSize: 16, height: 1.4),
        // ),
      ],
    ));

    // add detail
    if (kDebugMode) {
      children.add(Column(children: [
        const SizedBox(height: 30),
        const Text('DETAILS',
            textAlign: TextAlign.center,
            style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
        const SizedBox(height: 10),
        Text(errorDetails.exception.toString(),
            textAlign: TextAlign.start,
            style: const TextStyle(fontSize: 14, color: Colors.black87))
      ]));
    }

    overlayEntry = OverlayEntry(
      // Create a new OverlayEntry.
      builder: (BuildContext context) {
        // Align is used to position the highlight overlay
        // relative to the NavigationBar destination.
        return Scaffold(
          body: SafeArea(
            child: SingleChildScrollView(
              padding: const EdgeInsets.only(left: 40, right: 40, top: 40),
              child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: children),
            ),
          ),
        );
      },
    );

    // Add the OverlayEntry to the Overlay.
    isOverlay = true;
    Future.delayed(const Duration(milliseconds: 100), () {
      Overlay.of(context, debugRequiredFor: widget).insert(overlayEntry!);
    });
  }

  void removeErrorOverlay() {
    isOverlay = false;
    overlayEntry?.remove();
    overlayEntry = null;
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance
        .addPostFrameCallback((_) => createErrorOverlay(null));
  }

  @override
  void dispose() {
    // Make sure to remove OverlayEntry when the widget is disposed.
    removeErrorOverlay();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}