在 Flutter 或 Dart 项目中,应用启动往往不只是 runApp()

初始化日志、读取本地配置、打开数据库、恢复登录态、注册服务、拉取远程配置、埋点上报、缓存预热……随着业务增长,这些逻辑很容易全部堆进 main()

Future<void> main() async {
  await initLogger();
  await loadLocalConfig();
  await initDatabase();
  await restoreSession();
  await registerServices();
  await fetchRemoteConfig();

  runApp(const MyApp());
}

这种写法在项目初期足够直接,但任务越来越多后,通常会遇到几个问题:

  • 独立任务只能串行执行,启动时间被拉长;
  • 任务依赖只能靠人工维护调用顺序;
  • 超时、重试、失败降级逻辑散落在各处;
  • 首屏必须执行的任务与后台任务混在一起;
  • 启动失败时,难以快速定位具体任务和耗时。

为了解决这些问题,我开源了一个轻量级 Dart 包:startup_tasks

项目地址:


startup_tasks 是什么?

startup_tasks 用于将应用启动过程拆分为独立任务,并根据任务阶段与依赖关系自动调度。

它不依赖 Flutter,可以用于:

  • Flutter 应用;
  • 纯 Dart 项目;
  • CLI 工具;
  • 应用启动适配层。

启动任务可以按阶段划分:

bootstrap
   ↓
prepare
   ↓
business
   ↓
runApp
   ↓
postUi
   ↓
background

例如:

  • bootstrap:日志、异常捕获、运行时基础能力;
  • prepare:本地存储、数据库、设备信息;
  • business:登录态、用户信息、业务配置;
  • runApp:依赖注入、通道注册;
  • postUi:首帧后执行的任务;
  • background:不影响首屏的后台任务。

核心能力

startup_tasks 提供以下能力:

  • 按启动阶段组织任务;
  • 根据依赖关系自动调度任务;
  • 同阶段内支持并发执行与最大并发限制;
  • 依赖完成后立即启动后续任务;
  • 支持关键任务失败时中断启动;
  • 支持非关键任务失败后继续执行;
  • 支持任务超时与重试;
  • 支持协作式取消;
  • 通过类型安全的 StartupContext 在任务之间传递数据;
  • 生成结构化 StartupReport,用于日志和诊断;
  • 自动检查重复任务 ID、缺失依赖、循环依赖和跨阶段反向依赖。

安装

pubspec.yaml 中添加依赖:

dependencies:
  startup_tasks: ^1.0.2

导入公共库:

import 'package:startup_tasks/startup_tasks.dart';

快速开始

下面以“读取配置”和“注册服务”为例。

首先定义一个加载配置的任务:

import 'package:startup_tasks/startup_tasks.dart';

const apiBaseUrlKey = StartupKey<String>('apiBaseUrl');

final class LoadConfigTask extends StartupTask {
  const LoadConfigTask();

  @override
  String get id => 'app.load_config';

  @override
  StartupStage get stage => StartupStage.prepare;

  @override
  bool get critical => true;

  @override
  Future<void> run(StartupContext context) async {
    context.write(apiBaseUrlKey, 'https://api.example.com');
  }
}

然后定义依赖配置的服务注册任务:

final class RegisterServicesTask extends StartupTask {
  const RegisterServicesTask();

  @override
  String get id => 'app.register_services';

  @override
  StartupStage get stage => StartupStage.business;

  @override
  List<String> get dependencies => const ['app.load_config'];

  @override
  Future<void> run(StartupContext context) async {
    final apiBaseUrl = context.require(apiBaseUrlKey);

    print('Registering services for $apiBaseUrl');
  }
}

最后创建调度器并执行:

Future<void> main() async {
  final manager = StartupManager(
    tasks: const [
      LoadConfigTask(),
      RegisterServicesTask(),
    ],
  );

  final report = await manager.run();

  print(report.formatSummary());
}

这里的执行关系是:

app.load_config
        ↓
app.register_services

RegisterServicesTask 只有在 LoadConfigTask 成功后才会运行。配置数据通过 StartupContext 传递,无需依赖全局变量。


同阶段并发执行

对于没有依赖关系的任务,例如数据库初始化、设备信息读取和本地缓存加载,可以放在同一阶段并发执行。

final manager = StartupManager(
  tasks: const [
    InitDatabaseTask(),
    LoadDeviceInfoTask(),
    LoadLocalCacheTask(),
  ],
  maxConcurrentTasks: 3,
);

通过 maxConcurrentTasks 可以限制同一阶段内的最大并发数,避免启动时瞬间创建过多异步任务。


超时与重试

网络请求、远程配置等任务通常需要设置超时和重试策略。

final class FetchRemoteConfigTask extends StartupTask {
  const FetchRemoteConfigTask();

  @override
  String get id => 'app.fetch_remote_config';

  @override
  StartupStage get stage => StartupStage.business;

  @override
  StartupFailurePolicy get failurePolicy => StartupFailurePolicy.retry;

  @override
  int get maxRetryCount => 2;

  @override
  Duration? get timeout => const Duration(seconds: 3);

  @override
  Duration retryDelay(int retryCount) {
    return Duration(milliseconds: 100 * retryCount);
  }

  @override
  Future<void> run(StartupContext context) async {
    // 拉取远程配置。
  }
}

关键任务默认可以中断启动;非关键任务则可以记录失败后继续执行,让业务根据实际场景选择合适的策略。


分阶段执行,优化首屏体验

并不是所有初始化工作都必须阻塞首屏。

例如,在 UI 可用前只执行必要任务:

await manager.runUntil(StartupStage.business);

首屏展示后,再执行后续任务:

await manager.runAfter(StartupStage.business);

适合放到后续阶段的任务包括:

  • 埋点上报;
  • 缓存预热;
  • 资源预加载;
  • 非关键远程配置;
  • 后台数据同步。

这样可以把“应用可用”与“后台补偿工作”拆开,减少不必要的启动阻塞。


启动过程可观测

执行完成后会返回 StartupReport

final report = await manager.run();

print(report.succeeded);
print(report.formatSummary());

可以将报告写入日志、上传诊断系统,或展示在 Debug 页面中,用于排查:

  • 哪个任务失败;
  • 哪个任务超时;
  • 哪个任务发生重试;
  • 各任务执行耗时;
  • 是否仍有等待后续调度的任务。

结语

启动流程本质上是一个任务依赖图,而不是一串不断增长的 await

startup_tasks 尝试用一个小而独立的工具,将启动阶段、依赖关系、并发控制、失败策略和执行报告统一起来,让 Flutter/Dart 项目的初始化代码更清晰、更可维护、更容易定位问题。

项目地址:https://github.com/akumaCN/startup_tasks

欢迎 Star、Issue 和 PR。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐