diff --git a/example/lib/main.dart b/example/lib/main.dart index 14f8cb18..dd714e5e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import 'screens/configure_download/state/configure_download_provider.dart'; +import 'screens/initialisation_error/initialisation_error.dart'; import 'screens/main/main.dart'; import 'screens/main/pages/downloading/state/downloading_provider.dart'; import 'screens/main/pages/map/state/map_provider.dart'; @@ -20,55 +21,74 @@ void main() async { ), ); - // TODO: Implement error handling - await FMTCObjectBoxBackend().initialise(); + Object? initErr; + try { + await FMTCObjectBoxBackend().initialise(); + } catch (err) { + initErr = err; + } - runApp(const _AppContainer()); + runApp(_AppContainer(initialisationError: initErr)); } class _AppContainer extends StatelessWidget { - const _AppContainer(); + const _AppContainer({ + required this.initialisationError, + }); + + final Object? initialisationError; @override - Widget build(BuildContext context) => MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => GeneralProvider()), - ChangeNotifierProvider( - create: (_) => MapProvider(), - lazy: true, - ), - ChangeNotifierProvider( - create: (_) => RegionSelectionProvider(), - lazy: true, - ), - ChangeNotifierProvider( - create: (_) => ConfigureDownloadProvider(), - lazy: true, + Widget build(BuildContext context) { + final themeData = ThemeData( + brightness: Brightness.dark, + useMaterial3: true, + textTheme: GoogleFonts.ubuntuTextTheme(ThemeData.dark().textTheme), + colorSchemeSeed: Colors.red, + switchTheme: SwitchThemeData( + thumbIcon: MaterialStateProperty.resolveWith( + (states) => Icon( + states.contains(MaterialState.selected) ? Icons.check : Icons.close, ), - ChangeNotifierProvider( - create: (_) => DownloadingProvider(), - lazy: true, - ), - ], - child: MaterialApp( - title: 'FMTC Demo', - theme: ThemeData( - brightness: Brightness.dark, - useMaterial3: true, - textTheme: GoogleFonts.ubuntuTextTheme(const TextTheme()), - colorSchemeSeed: Colors.red, - switchTheme: SwitchThemeData( - thumbIcon: MaterialStateProperty.resolveWith( - (states) => Icon( - states.contains(MaterialState.selected) - ? Icons.check - : Icons.close, - ), - ), - ), - ), - debugShowCheckedModeBanner: false, - home: const MainScreen(), ), + ), + ); + + if (initialisationError case final err?) { + return MaterialApp( + title: 'FMTC Demo (Initialisation Error)', + theme: themeData, + home: InitialisationError(err: err), ); + } + + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => GeneralProvider(), + ), + ChangeNotifierProvider( + create: (_) => MapProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => RegionSelectionProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => ConfigureDownloadProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => DownloadingProvider(), + lazy: true, + ), + ], + child: MaterialApp( + title: 'FMTC Demo', + theme: themeData, + home: const MainScreen(), + ), + ); + } } diff --git a/example/lib/screens/export_import/components/import.dart b/example/lib/screens/export_import/components/import.dart index db09d31e..e514e37e 100644 --- a/example/lib/screens/export_import/components/import.dart +++ b/example/lib/screens/export_import/components/import.dart @@ -113,15 +113,13 @@ class _ImportState extends State { title: Text(storeName), subtitle: FutureBuilder( future: FMTCStore(storeName).manage.ready, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Text('Checking for conflicts...'); - } - if (snapshot.data!) { - return const Text('Conflicts with existing store'); - } - return const SizedBox.shrink(); - }, + builder: (context, snapshot) => Text( + switch (snapshot.data) { + null => 'Checking for conflicts...', + true => 'Conflicts with existing store', + false => 'No conflicts', + }, + ), ), dense: true, trailing: Row( diff --git a/example/lib/screens/initialisation_error/initialisation_error.dart b/example/lib/screens/initialisation_error/initialisation_error.dart new file mode 100644 index 00000000..7cae2a7b --- /dev/null +++ b/example/lib/screens/initialisation_error/initialisation_error.dart @@ -0,0 +1,105 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +import '../../main.dart'; + +class InitialisationError extends StatelessWidget { + const InitialisationError({super.key, required this.err}); + + final Object? err; + + @override + Widget build(BuildContext context) => Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(32), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, size: 64), + const SizedBox(height: 12), + Text( + 'Whoops, look like FMTC ran into an error initialising', + style: Theme.of(context) + .textTheme + .displaySmall! + .copyWith(color: Colors.white), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + SelectableText( + 'Type: ${err.runtimeType}', + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + SelectableText( + 'Error: $err', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Text( + 'We recommend trying to delete the existing root, as it may ' + 'have become corrupt.\nPlease be aware that this will delete ' + 'any cached data, and will cause the app to restart.', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Colors.white), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + OutlinedButton( + onPressed: () async { + void showFailure() { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + "Unfortuately, that didn't work. Try clearing " + "the app's storage and cache manually.", + ), + ), + ); + } + } + + final dir = Directory( + path.join( + (await getApplicationDocumentsDirectory()) + .absolute + .path, + 'fmtc', + ), + ); + + if (!await dir.exists()) { + showFailure(); + return; + } + + try { + await dir.delete(recursive: true); + } on FileSystemException { + showFailure(); + rethrow; + } + + runApp(const SizedBox.shrink()); + + main(); + }, + child: const Text( + 'Reset FMTC & attempt re-initialisation', + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ); +} diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index 356e5f91..48464765 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -54,6 +54,7 @@ class DownloadLayout extends StatelessWidget { const SizedBox(width: 16), Expanded( child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ TableRow( children: [ @@ -73,12 +74,12 @@ class DownloadLayout extends StatelessWidget { children: [ StatDisplay( statistic: - '${download.skippedTiles} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.skippedTiles) / download.cachedTiles) * 100).toStringAsFixed(1)}%)', + '${download.skippedTiles} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.skippedTiles) / download.cachedTiles) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', description: 'skipped tiles (% saving)', ), StatDisplay( statistic: - '${(download.skippedSize * 1024).asReadableSize} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.skippedSize) / download.cachedSize) * 100).toStringAsFixed(1)}%)', + '${(download.skippedSize * 1024).asReadableSize} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.skippedSize) / download.cachedSize) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', description: 'skipped size (% saving)', ), ], diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index f113d800..9a15b526 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -108,6 +108,7 @@ class _DownloadingPageState extends State ); } + // TODO: Make responsive return DownloadLayout( storeDirectory: context.select( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index da161563..09ff84c5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: latlong2: ^0.9.0 osm_nominatim: ^3.0.0 path: ^1.9.0 + path_provider: ^2.1.2 provider: ^6.1.2 stream_transform: ^2.1.0 validators: ^3.0.0 diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index ec03e17a..0c35c8b1 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -22,6 +22,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { void get expectInitialised => _sendPort ?? (throw RootUnavailable()); // Worker communication protocol storage + SendPort? _sendPort; final _workerResOneShot = ?>>{}; final _workerResStreamed = ?>>{}; @@ -30,9 +31,12 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { late StreamSubscription _workerHandler; // `removeOldestTilesAboveLimit` tracking & debouncing + Timer? _rotalDebouncer; String? _rotalStore; + // Define communicators + Future?> _sendCmdOneShot({ required _WorkerCmdType type, Map args = const {}, @@ -99,6 +103,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { } } + // Lifecycle implementations + Future initialise({ required String? rootDirectory, required int maxDatabaseSize, @@ -249,6 +255,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { FMTCBackendAccessThreadSafe.internal = null; } + // Implementation & worker connectors + @override Future realSize() async => (await _sendCmdOneShot(type: _WorkerCmdType.realSize))!['size'];