import 'dart:async'; import 'dart:io'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/startup/tasks/feature_flag_task.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'deps_resolver.dart'; import 'entry_point.dart'; import 'launch_configuration.dart'; import 'plugin/plugin.dart'; import 'tasks/prelude.dart'; final getIt = GetIt.instance; abstract class EntryPoint { Widget create(LaunchConfiguration config); } class FlowyRunnerContext { FlowyRunnerContext({required this.applicationDataDirectory}); final Directory applicationDataDirectory; } Future runAppFlowy({bool isAnon = false}) async { if (kReleaseMode) { await FlowyRunner.run( AppFlowyApplication(), integrationMode(), isAnon: isAnon, ); } else { // When running the app in integration test mode, we need to // specify the mode to run the app again. await FlowyRunner.run( AppFlowyApplication(), FlowyRunner.currentMode, didInitGetItCallback: IntegrationTestHelper.didInitGetItCallback, rustEnvsBuilder: IntegrationTestHelper.rustEnvsBuilder, isAnon: isAnon, ); } } class FlowyRunner { // This variable specifies the initial mode of the app when it is launched for the first time. // The same mode will be automatically applied in subsequent executions when the runAppFlowy() // method is called. static var currentMode = integrationMode(); static Future run( EntryPoint f, IntegrationMode mode, { // This callback is triggered after the initialization of 'getIt', // which is used for dependency injection throughout the app. // If your functionality depends on 'getIt', ensure to register // your callback here to execute any necessary actions post-initialization. Future Function()? didInitGetItCallback, // Passing the envs to the backend Map Function()? rustEnvsBuilder, // Indicate whether the app is running in anonymous mode. // Note: when the app is running in anonymous mode, the user no need to // sign in, and the app will only save the data in the local storage. bool isAnon = false, }) async { currentMode = mode; // Only set the mode when it's not release mode if (!kReleaseMode) { IntegrationTestHelper.didInitGetItCallback = didInitGetItCallback; IntegrationTestHelper.rustEnvsBuilder = rustEnvsBuilder; } // Clear and dispose tasks from previous AppLaunch if (getIt.isRegistered(instance: AppLauncher)) { await getIt().dispose(); } // Clear all the states in case of rebuilding. await getIt.reset(); final config = LaunchConfiguration( isAnon: isAnon, // Unit test can't use the package_info_plus plugin version: mode.isUnitTest ? '1.0.0' : await PackageInfo.fromPlatform().then((value) => value.version), rustEnvs: rustEnvsBuilder?.call() ?? {}, ); // Specify the env await initGetIt(getIt, mode, f, config); await didInitGetItCallback?.call(); final applicationDataDirectory = await getIt().getPath().then( (value) => Directory(value), ); // add task final launcher = getIt(); launcher.addTasks( [ // this task should be first task, for handling platform errors. // don't catch errors in test mode if (!mode.isUnitTest) const PlatformErrorCatcherTask(), // this task should be second task, for handling memory leak. // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored. MemoryLeakDetectorTask(), const DebugTask(), const FeatureFlagTask(), // localization const InitLocalizationTask(), // init the app window InitAppWindowTask(), // Init Rust SDK InitRustSDKTask(customApplicationPath: applicationDataDirectory), // Load Plugins, like document, grid ... const PluginLoadTask(), // init the app widget // ignore in test mode if (!mode.isUnitTest) ...[ // The DeviceOrApplicationInfoTask should be placed before the AppWidgetTask to fetch the app information. // It is unable to get the device information from the test environment. const ApplicationInfoTask(), const HotKeyTask(), if (isSupabaseEnabled) InitSupabaseTask(), if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(), const InitAppWidgetTask(), const InitPlatformServiceTask(), const RecentServiceTask(), ], ], ); await launcher.launch(); // execute the tasks return FlowyRunnerContext( applicationDataDirectory: applicationDataDirectory, ); } } Future initGetIt( GetIt getIt, IntegrationMode mode, EntryPoint f, LaunchConfiguration config, ) async { getIt.registerFactory(() => f); getIt.registerLazySingleton( () { return FlowySDK(); }, dispose: (sdk) async { await sdk.dispose(); }, ); getIt.registerLazySingleton( () => AppLauncher( context: LaunchContext( getIt, mode, config, ), ), dispose: (launcher) async { await launcher.dispose(); }, ); getIt.registerSingleton(PluginSandbox()); await DependencyResolver.resolve(getIt, mode); } class LaunchContext { LaunchContext(this.getIt, this.env, this.config); GetIt getIt; IntegrationMode env; LaunchConfiguration config; } enum LaunchTaskType { dataProcessing, appLauncher, } /// The interface of an app launch task, which will trigger /// some nonresident indispensable task in app launching task. abstract class LaunchTask { const LaunchTask(); LaunchTaskType get type => LaunchTaskType.dataProcessing; Future initialize(LaunchContext context); Future dispose(); } class AppLauncher { AppLauncher({ required this.context, }); final LaunchContext context; final List tasks = []; void addTask(LaunchTask task) { tasks.add(task); } void addTasks(Iterable tasks) { this.tasks.addAll(tasks); } Future launch() async { for (final task in tasks) { await task.initialize(context); } } Future dispose() async { for (final task in tasks) { await task.dispose(); } tasks.clear(); } } enum IntegrationMode { develop, release, unitTest, integrationTest; // test mode bool get isTest => isUnitTest || isIntegrationTest; bool get isUnitTest => this == IntegrationMode.unitTest; bool get isIntegrationTest => this == IntegrationMode.integrationTest; // release mode bool get isRelease => this == IntegrationMode.release; // develop mode bool get isDevelop => this == IntegrationMode.develop; } IntegrationMode integrationMode() { if (Platform.environment.containsKey('FLUTTER_TEST')) { return IntegrationMode.unitTest; } if (kReleaseMode) { return IntegrationMode.release; } return IntegrationMode.develop; } /// Only used for integration test class IntegrationTestHelper { static Future Function()? didInitGetItCallback; static Map Function()? rustEnvsBuilder; }