Understanding Two-Phase Initialization
What you'll learn
- The complete boot sequence from
main()to first frame - How
bootstrap.dart,HostAppWidget,PreLaunchInitializer, andPostLaunchInitializerwork together - What runs in pre-launch (blocking) vs post-launch (non-blocking)
- How the
Initializerbase class supports sequential and parallel execution
The problem
Flutter apps need to initialize many things at startup: DI containers, routers, analytics, feature flags, remote config. Two bad extremes exist:
- Block on everything -- The splash screen hangs for seconds while analytics and remote config load.
- Make everything async -- The app renders before routes are registered. Navigation crashes.
The solution: two phases
| Phase | Timing | Failure handling |
|---|---|---|
| Pre-launch | Before any UI renders | Show error screen, app cannot continue |
| Post-launch | After first frame paints | Degrade gracefully, app still works |
The bootstrap entry point
Everything starts in bootstrap.dart. It runs inside runZonedGuarded for top-level error handling:
// apps/template_app/lib/bootstrap.dart
Future<void> bootstrap({
required FlavorStatus flavor,
required AppBuilder builder,
}) async => runZonedGuarded(
() async {
Flavor.status = flavor;
WidgetsFlutterBinding.ensureInitialized();
await initializeInjector();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
final benchmark = StartupBenchmark(mode: flavor.name)..startApp();
runApp(
HostAppWidget(
benchmark: benchmark,
initializerController: PreLaunchInitializerController.create(),
builder: (context, result) => builder(result, benchmark),
),
);
},
(error, stackTrace) {
debugPrint('Uncaught error: $error');
debugPrint('Stack trace: $stackTrace');
},
);Note that initializeInjector() is called before runApp(). By the time HostAppWidget mounts, all DI modules are already registered and FeatureSDKs have self-registered with FeatureSDKRegistry.
The Initializer base class
Both pre-launch and post-launch initializers extend Initializer:
abstract class Initializer {
const Initializer();
bool get canRunInParallel => true;
Future<InitializerResult> initialize();
Future<bool> dispose();
}The canRunInParallel flag controls execution: sequential initializers run first (in order), then parallel initializers run concurrently via Future.wait.
InitializerResult carries success status and an optional AppRouter:
class InitializerResult {
const InitializerResult.success({this.router});
const InitializerResult.failure(this.error);
final bool isInitializationSuccessful;
final AppRouter? router;
final Object? error;
AppRouter get requireRouter => router!;
}Phase 1: PreLaunchInitializer (blocking)
PreLaunchInitializerController wraps PreLaunchInitializer and is passed to HostAppWidget. The initialize() method in HostAppWidget.initState() triggers this sequence:
- Run sequential initializers (in order)
- Run parallel initializers (concurrently via
Future.wait) - Discover SDKs from
FeatureSDKRegistry.instance.sdks - Call
sdk.registerRoutes(routeRegistry)on each SDK - Call
routeRegistry.lock()-- no more routes after this - Call
sdk.initialize()on each SDK - Return
InitializerResult.success(router: AppRouter(routeRegistry))
// Simplified from PreLaunchInitializer.initialize()
final sdks = FeatureSDKRegistry.instance.sdks;
final routeRegistry = getIt<RouteRegistry>();
for (final sdk in sdks) {
sdk.registerRoutes(routeRegistry);
}
routeRegistry.lock();
for (final sdk in sdks) {
await sdk.initialize();
}
return InitializerResult.success(router: AppRouter(routeRegistry));If any sequential or parallel initializer fails, initialization aborts and returns the failure result.
Phase 2: PostLaunchInitializer (non-blocking)
Post-launch runs after the first frame is painted, scheduled via addPostFrameCallback in HostAppWidget.initState(). It follows the same sequential-then-parallel pattern as pre-launch, but failures here do not crash the app.
// apps/template_app/lib/host/initializers/post_launch_initializer.dart
factory PostLaunchInitializer.create({
required BuildContext context,
required AppRouter router,
}) => PostLaunchInitializer._([
// TODO: Add post-launch initializers here.
// Examples: AnalyticsInitializer, FeatureFlagInitializer
]);Currently the post-launch initializer list is empty in the template. When you add features like analytics or feature flags, you add them here.
HostAppWidget: the orchestrator
HostAppWidget ties both phases together. Pre-launch runs in initState(), and the result drives a FutureBuilder with AnimatedSwitcher for smooth transitions:
HostAppWidget.initState()
|-- PreLaunchInitializerController.initialize()
| |-- Sequential initializers (in order)
| |-- Parallel initializers (concurrent)
| |-- FeatureSDK route registration
| |-- RouteRegistry.lock()
| |-- FeatureSDK initialization
| \-- Return InitializerResult with AppRouter
|
|-- Completer.complete(result) --> FutureBuilder rebuilds
| \-- AnimatedSwitcher: loading -> app (FadeTransition)
|
\-- addPostFrameCallback()
|-- benchmark.markFirstFrameRendered()
\-- PostLaunchInitializer.create().initialize()
|-- Sequential initializers
|-- Parallel initializers
\-- benchmark.printSummary()The widget shows three states via AnimatedSwitcher:
- Loading (default
CircularProgressIndicator) -- while pre-launch runs - Error -- if pre-launch fails, shows error message
- App -- on success, renders
builder(context, result)with theAppRouter
The AnimatedSwitcher uses a FadeTransition by default (300ms duration), and each state is wrapped in a KeyedSubtree with a ValueKey ('loading', 'error', 'app') so the switcher correctly animates between them.
StartupBenchmark
HostAppWidget records timing milestones via StartupBenchmark:
startApp()-- called beforerunApp()markPreLaunchCompleted()-- after pre-launch finishesmarkFirstFrameRendered()-- inaddPostFrameCallbackstartPostLaunch()/finishPostLaunch()-- around post-launchprintSummary()-- logs all durations to console
Summary
| Concern | Phase | Blocking? | On failure |
|---|---|---|---|
| DI container setup | bootstrap (before runApp) | Yes | Zone error |
| Route registration | Pre-launch | Yes | Error screen |
| SDK initialization | Pre-launch | Yes | Error screen |
| Router creation | Pre-launch | Yes | Error screen |
| Analytics | Post-launch | No | Degrade gracefully |
| Feature flags | Post-launch | No | Use defaults |
Next steps
- GetIt Dependency Injection -- How global DI and microPackage modules manage dependencies
- FeatureSDK Self-Registration -- How features register themselves automatically