diff --git a/firka_wear/lib/app/app_state.dart b/firka_wear/lib/app/app_state.dart index 389de28..8dd0e12 100644 --- a/firka_wear/lib/app/app_state.dart +++ b/firka_wear/lib/app/app_state.dart @@ -9,6 +9,10 @@ import 'package:firka_wear/services/wear_sync_store.dart'; late final Logger logger; final GlobalKey navigatorKey = GlobalKey(); + +/// When non-null, the app should show [WearErrorScreen] with this error. +final ValueNotifier globalErrorNotifier = + ValueNotifier(null); late WearAppInitialization initData; bool initDone = false; diff --git a/firka_wear/lib/app/initialization_screen.dart b/firka_wear/lib/app/initialization_screen.dart index 8815ac6..e53bb9b 100644 --- a/firka_wear/lib/app/initialization_screen.dart +++ b/firka_wear/lib/app/initialization_screen.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:wear_plus/wear_plus.dart'; import 'package:firka_wear/app/app_state.dart'; import 'package:firka_wear/app/initialization.dart'; import 'package:firka_wear/core/bloc/wear_sync_cubit.dart'; import 'package:firka_wear/l10n/app_localizations.dart'; import 'package:firka_wear/ui/theme/style.dart'; +import 'package:firka_wear/ui/wear/screens/error/error_screen.dart'; import 'package:firka_wear/ui/wear/screens/home/home_screen.dart'; import 'package:firka_wear/ui/wear/screens/login/login_screen.dart'; @@ -25,25 +25,7 @@ class WearInitializationScreen extends StatelessWidget { if (snapshot.hasError) { return MaterialApp( key: ValueKey('firkaErrorPage'), - home: Scaffold( - body: Center( - child: WatchShape( - builder: (context, shape, child) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Error initializing app: ${snapshot.error}', - style: TextStyle(color: Colors.red), - ), - child!, - ], - ); - }, - child: SizedBox(), - ), - ), - ), + home: WearErrorScreen(exception: snapshot.error!), ); } diff --git a/firka_wear/lib/main.dart b/firka_wear/lib/main.dart index 6505ae3..b534440 100644 --- a/firka_wear/lib/main.dart +++ b/firka_wear/lib/main.dart @@ -1,7 +1,9 @@ +import 'dart:async'; import 'dart:io'; import 'package:firka_wear/app/app_state.dart'; import 'package:firka_wear/app/initialization_screen.dart'; +import 'package:firka_wear/ui/wear/screens/error/error_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:logging/logging.dart'; @@ -26,5 +28,49 @@ void main() async { await ScreenUtil.ensureScreenSize(); - runApp(WearInitializationScreen()); + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + if (_isFatalError(details.exception)) { + globalErrorNotifier.value = details; + } + }; + + runZonedGuarded(() => runApp(const _WearAppWrapper()), ( + Object error, + StackTrace stackTrace, + ) { + if (_isFatalError(error)) { + globalErrorNotifier.value = FlutterErrorDetails( + exception: error, + stack: stackTrace, + library: 'firka_wear', + ); + } + }); +} + +bool _isFatalError(Object error) { + return error is! AssertionError; +} + +class _WearAppWrapper extends StatelessWidget { + const _WearAppWrapper(); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: globalErrorNotifier, + builder: (context, error, _) { + if (error != null) { + return MaterialApp( + home: WearErrorScreen( + exception: error.exception, + stackTrace: error.stack, + ), + ); + } + return WearInitializationScreen(); + }, + ); + } } diff --git a/firka_wear/lib/ui/wear/screens/error/error_screen.dart b/firka_wear/lib/ui/wear/screens/error/error_screen.dart new file mode 100644 index 0000000..79c13db --- /dev/null +++ b/firka_wear/lib/ui/wear/screens/error/error_screen.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:wear_plus/wear_plus.dart'; + +import 'package:firka_wear/ui/theme/style.dart'; + +final int _kMaxQrPayloadChars = 410; + +String errorPayload(Object exception, [StackTrace? stackTrace]) { + final buffer = StringBuffer(); + buffer.writeln(exception.toString()); + if (stackTrace != null) buffer.write(stackTrace.toString()); + final s = buffer.toString(); + return s.length > _kMaxQrPayloadChars + ? s.substring(0, _kMaxQrPayloadChars) + : s; +} + +/// Full-screen error UI: encodes [exception] (and [stackTrace]) into a QR code +/// scaled to fit the watch's circular display so it is not clipped. +class WearErrorScreen extends StatelessWidget { + final Object exception; + final StackTrace? stackTrace; + + const WearErrorScreen({super.key, required this.exception, this.stackTrace}); + + @override + Widget build(BuildContext context) { + ScreenUtil.init(context); + + final payload = errorPayload(exception, stackTrace); + return Scaffold( + backgroundColor: wearStyle.colors.background, + body: LayoutBuilder( + builder: (context, constraints) { + return Center( + child: WatchShape( + builder: (context, shape, child) { + return SizedBox( + width: 350.w, + height: 350.h, + child: QrImageView( + data: payload, + version: 13, + backgroundColor: wearStyle.colors.background, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: wearStyle.colors.textPrimary, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: wearStyle.colors.textPrimary, + ), + ), + ); + }, + child: const SizedBox.shrink(), + ), + ); + }, + ), + ); + } +} diff --git a/firka_wear/pubspec.yaml b/firka_wear/pubspec.yaml index 46fc034..e4fab93 100644 --- a/firka_wear/pubspec.yaml +++ b/firka_wear/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: flutter_svg: ^1.1.6 logging: ^1.3.0 flutter_bloc: ^9.0.0 + qr_flutter: ^4.1.0 dev_dependencies: build_runner: any