import 'dart:async'; import 'package:device_preview/device_preview.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:openfoodfacts/personalized_search/product_preferences_selection.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'package:scanner_shared/scanner_shared.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:smooth_app/background/background_task_manager.dart'; import 'package:smooth_app/data_models/continuous_scan_model.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/user_management_provider.dart'; import 'package:smooth_app/data_models/user_preferences.dart'; import 'package:smooth_app/database/dao_string.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; import 'package:smooth_app/helpers/camera_helper.dart'; import 'package:smooth_app/helpers/data_importer/smooth_app_data_importer.dart'; import 'package:smooth_app/helpers/network_config.dart'; import 'package:smooth_app/helpers/permission_helper.dart'; import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart'; import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/services/smooth_services.dart'; import 'package:smooth_app/themes/smooth_theme.dart'; import 'package:smooth_app/themes/theme_provider.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; late bool _screenshots; Future launchSmoothApp({ required CameraScanner scanner, final bool screenshots = false, }) async { _screenshots = screenshots; if (_screenshots) { await _init1(); runApp(SmoothApp(scanner)); return; } final WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); if (kReleaseMode) { await AnalyticsHelper.initSentry( appRunner: () => runApp(SmoothApp(scanner)), ); } else { runApp( DevicePreview( enabled: true, builder: (_) => SmoothApp(scanner), ), ); } } class SmoothApp extends StatefulWidget { const SmoothApp(this.scanner); final CameraScanner scanner; // This widget is the root of your application @override State createState() => _SmoothAppState(); } late SmoothAppDataImporter _appDataImporter; late UserPreferences _userPreferences; late ProductPreferences _productPreferences; late LocalDatabase _localDatabase; late ThemeProvider _themeProvider; final ContinuousScanModel _continuousScanModel = ContinuousScanModel(); final PermissionListener _permissionListener = PermissionListener(permission: Permission.camera); bool _init1done = false; // Had to split init in 2 methods, for test/screenshots reasons. // Don't know why, but some init codes seem to freeze the test. // Now we run them before running the app, during the tests. Future _init1() async { if (_init1done) { return false; } await SmoothServices().init(); await setupAppNetworkConfig(); await UserManagementProvider.mountCredentials(); _userPreferences = await UserPreferences.getUserPreferences(); _localDatabase = await LocalDatabase.getLocalDatabase(); _appDataImporter = SmoothAppDataImporter(_localDatabase); await _continuousScanModel.load(_localDatabase); _productPreferences = ProductPreferences( ProductPreferencesSelection( setImportance: _userPreferences.setImportance, getImportance: _userPreferences.getImportance, notify: () => _productPreferences.notifyListeners(), ), daoString: DaoString(_localDatabase), ); BackgroundTaskManager(_localDatabase).run(); UserManagementProvider().checkUserLoginValidity(); AnalyticsHelper.setCrashReports(_userPreferences.crashReports); ProductQuery.setCountry(_userPreferences.userCountryCode); _themeProvider = ThemeProvider(_userPreferences); ProductQuery.setQueryType(_userPreferences); await CameraHelper.init(); await ProductQuery.setUuid(_localDatabase); _init1done = true; return true; } class _SmoothAppState extends State { final UserManagementProvider _userManagementProvider = UserManagementProvider(); bool systemDarkmodeOn = false; final Brightness brightness = SchedulerBinding.instance.window.platformBrightness; // We store the argument of FutureBuilder to avoid re-initialization on // subsequent builds. This enables hot reloading. See // https://github.com/openfoodfacts/smooth-app/issues/473 late Future _initFuture; @override void initState() { super.initState(); _initFuture = _init2(); } Future _init2() async { await _init1(); systemDarkmodeOn = brightness == Brightness.dark; if (!mounted) { return false; } await _productPreferences.init(DefaultAssetBundle.of(context)); await AnalyticsHelper.initMatomo(_screenshots); if (!_screenshots) { await _userPreferences.init(_productPreferences); } return true; } @override Widget build(BuildContext context) { return FutureBuilder( future: _initFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { FlutterNativeSplash.remove(); return _buildError(snapshot); } if (snapshot.connectionState != ConnectionState.done) { //We don't need a loading indicator since the splash screen is still visible return Container(); } // The `create` constructor of [ChangeNotifierProvider] takes care of // disposing the value. ChangeNotifierProvider provide(T value) => ChangeNotifierProvider(create: (BuildContext context) => value); if (!_screenshots) { // ending FlutterNativeSplash.preserve() FlutterNativeSplash.remove(); } return MultiProvider( providers: [ provide(_userPreferences), provide(_productPreferences), provide(_localDatabase), provide(_themeProvider), provide(_userManagementProvider), provide(_continuousScanModel), provide(_appDataImporter), provide(_permissionListener), provide( CameraHelper.cameraControllerNotifier, ), Provider.value( value: widget.scanner, ), ], builder: _buildApp, ); }, ); } Widget _buildApp(BuildContext context, Widget? child) { final ThemeProvider themeProvider = context.watch(); final OnboardingPage lastVisitedOnboardingPage = _userPreferences.lastVisitedOnboardingPage; final Widget appWidget = OnboardingFlowNavigator(_userPreferences) .getPageWidget(context, lastVisitedOnboardingPage); final bool isOnboardingComplete = OnboardingFlowNavigator.isOnboardingComplete(lastVisitedOnboardingPage); themeProvider.setOnboardingComplete(isOnboardingComplete); final String? languageCode = context.select((UserPreferences up) => up.appLanguageCode); return MaterialApp( locale: languageCode != null ? Locale(languageCode) : null, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, debugShowCheckedModeBanner: !(kReleaseMode || _screenshots), navigatorObservers: [ SentryNavigatorObserver(), ], theme: SmoothTheme.getThemeData( Brightness.light, themeProvider, ), darkTheme: SmoothTheme.getThemeData( Brightness.dark, themeProvider, ), themeMode: themeProvider.currentThemeMode, home: SmoothAppGetLanguage(appWidget), ); } Widget _buildError(AsyncSnapshot snapshot) { return MaterialApp( home: SmoothScaffold( body: Center( child: Text( 'Fatal Error: ${snapshot.error}', ), ), ), ); } } /// Layer needed because we need to know the language. Language isn't available /// in the [context] in top level widget ([SmoothApp]) class SmoothAppGetLanguage extends StatelessWidget { const SmoothAppGetLanguage(this.appWidget); final Widget appWidget; @override Widget build(BuildContext context) { // TODO(monsieurtanuki): refactor removing the `SmoothAppGetLanguage` layer? // will refresh each time the language changes context.select( (final UserPreferences userPreferences) => userPreferences.appLanguageCode, ); final String languageCode = Localizations.localeOf(context).languageCode; ProductQuery.setLanguage(languageCode); context.read().refresh(languageCode); // The migration requires the language to be set in the app _appDataImporter.startMigrationAsync(); return appWidget; } }