告别臃肿的 main():一个用于 Flutter/Dart 启动任务编排的轻量级库
在 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。
更多推荐

所有评论(0)