精选文章

Flutter 启动实战代码

2023-08-14 · 工程化

说明:本文使用可直接复用的代码范式,不绑定具体业务与项目。

这篇文章不做泛泛而谈,而是给出一套可直接复用的启动代码结构:分层初始化、依赖注入、延迟任务、错误边界。你可以直接把这些代码放进项目,替代“一个 main 写到底”的做法。

1. 先把启动拆成“步骤”

启动最大的痛点是“不可测试”。解决方案是把启动拆成可组合的步骤:

abstract class StartupTask {
  String get name;
  Future<void> run(StartupContext context);
}

class StartupContext {
  final Stopwatch stopwatch;
  final AppContainer container;

  StartupContext({required this.stopwatch, required this.container});
}

这样每一步都可以独立测试,也可以统计耗时。

2. 依赖容器:集中创建、统一管理生命周期

核心依赖不要散落在各个 Widget 里,统一放到容器中:

class AppContainer {
  final LocalStore store;
  final ApiClient api;
  final AuthService auth;

  AppContainer({required this.store, required this.api, required this.auth});

  static Future<AppContainer> build(Env env) async {
    final store = await LocalStore.init();
    final api = ApiClient(baseUrl: env.baseUrl);
    final auth = AuthService(api: api, store: store);
    return AppContainer(store: store, api: api, auth: auth);
  }
}

关键点:容器只做构建,不做业务逻辑。

3. 启动编排器:按顺序执行并记录耗时

class Bootstrapper {
  final List<StartupTask> tasks;

  Bootstrapper(this.tasks);

  Future<void> run(StartupContext context) async {
    for (final task in tasks) {
      final before = context.stopwatch.elapsedMilliseconds;
      await task.run(context);
      final cost = context.stopwatch.elapsedMilliseconds - before;
      Log.d('startup', '${task.name} ${cost}ms');
    }
  }
}

这段代码让“启动耗时”具备可观测性。

4. 把“必须同步”和“可延迟”拆开

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final env = await EnvLoader.load();
  final container = await AppContainer.build(env);

  final context = StartupContext(
    stopwatch: Stopwatch()..start(),
    container: container,
  );

  final bootstrapper = Bootstrapper([
    InitCrashReporter(),
    InitUserSession(),
    InitTheme(),
  ]);

  await bootstrapper.run(context);

  runApp(AppBootstrap(container));

  // 延迟任务
  unawaited(BackgroundInit.run(container));
}

启动时只执行“必要任务”,其余任务放到后台。

5. 延迟初始化任务的范式

class BackgroundInit {
  static Future<void> run(AppContainer container) async {
    await Future.wait([
      Analytics.init(),
      RemoteConfig.refresh(),
      ImSdk.init(container.auth),
    ]);
  }
}

关键点:后台任务必须可失败但不阻塞主流程。

6. 错误边界:让启动阶段可观测

class InitCrashReporter implements StartupTask {
  @override
  String get name => 'CrashReporter';

  @override
  Future<void> run(StartupContext context) async {
    await CrashReporter.init();
    FlutterError.onError = (details) {
      CrashReporter.report(details.exception, details.stack);
    };
  }
}

启动阶段的异常如果不可见,就是最危险的异常。

7. 依赖注入与状态管理分离

不要在 Provider/Bloc 里创建服务依赖:

class AppBootstrap extends StatelessWidget {
  final AppContainer container;
  const AppBootstrap(this.container, {super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider.value(value: container.api),
        ChangeNotifierProvider(create: (_) => SessionModel(container.auth)),
      ],
      child: const MyApp(),
    );
  }
}

这样 UI 只关心状态,不关心服务创建。

8. 一份可复用的启动清单

  • 运行时绑定 / Crash 监控
  • 环境配置与本地存储
  • 依赖容器构建
  • 启动步骤执行与耗时记录
  • 延迟初始化后台任务
  • Provider/状态注入

总结

一套可维护的启动链路,必须做到:

  • 可拆分:每一步都是可测试任务
  • 可观测:每一步都能统计耗时
  • 可延迟:重任务不阻塞首屏

这样才能让 Flutter 项目在规模扩展时保持稳定。

JJ

作者简介

专注于内容创作、产品策略与设计实践。欢迎交流合作。

上一篇 下一篇