Skip to content

Understanding Two-Phase Initialization

What you'll learn

  • The complete boot sequence from main() to first frame
  • How bootstrap.dart, HostAppWidget, PreLaunchInitializer, and PostLaunchInitializer work together
  • What runs in pre-launch (blocking) vs post-launch (non-blocking)
  • How the Initializer base 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

PhaseTimingFailure handling
Pre-launchBefore any UI rendersShow error screen, app cannot continue
Post-launchAfter first frame paintsDegrade gracefully, app still works

The bootstrap entry point

Everything starts in bootstrap.dart. It runs inside runZonedGuarded for top-level error handling:

dart
// 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:

dart
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:

dart
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:

  1. Run sequential initializers (in order)
  2. Run parallel initializers (concurrently via Future.wait)
  3. Discover SDKs from FeatureSDKRegistry.instance.sdks
  4. Call sdk.registerRoutes(routeRegistry) on each SDK
  5. Call routeRegistry.lock() -- no more routes after this
  6. Call sdk.initialize() on each SDK
  7. Return InitializerResult.success(router: AppRouter(routeRegistry))
dart
// 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.

dart
// 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 the AppRouter

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 before runApp()
  • markPreLaunchCompleted() -- after pre-launch finishes
  • markFirstFrameRendered() -- in addPostFrameCallback
  • startPostLaunch() / finishPostLaunch() -- around post-launch
  • printSummary() -- logs all durations to console

Summary

ConcernPhaseBlocking?On failure
DI container setupbootstrap (before runApp)YesZone error
Route registrationPre-launchYesError screen
SDK initializationPre-launchYesError screen
Router creationPre-launchYesError screen
AnalyticsPost-launchNoDegrade gracefully
Feature flagsPost-launchNoUse defaults

Next steps

Built by Banua Coder