A pure Dart orchestration engine for app bootstrap flows. Define ordered startup work, defer post-auth phases behind named gates, and handle retries, timeouts, and cancellation - all without depending on Flutter, Riverpod, BLoC, or any other framework.
- Ordered steps with explicit dependencies
- Named gates for deferred work (not hard-coded phases)
- Built-in
InitializationGate.authenticatedconvenience gate - Per-step
shouldRunpredicates for conditional execution - Per-step or plan-level timeouts
- Severity-based failure handling:
skippable,retryable,fatal - Cooperative cancellation with
InitializationCancellationToken - Append-only execution journal
- Immutable snapshots with per-step state
- Aggregate
InitializationSummaryfor high-level reporting - Framework-agnostic: inject your own context through the generic
hostparameter
Add the dependency to your pubspec.yaml:
dependencies:
initialization_manager: ^0.1.0Or run:
dart pub add initialization_managerimport 'package:initialization_manager/initialization_manager.dart';
class AppHost {
Future<void> initializeCore() async {}
Future<String?> restoreSession() async => null;
Future<void> preloadGuestContent() async {}
Future<void> fetchPackages(String token) async {}
}
final engine = InitializationEngine<AppHost>(
host: AppHost(),
plan: InitializationPlan<AppHost>(
steps: <InitializationStep<AppHost>>[
InitializationStep<AppHost>(
id: 'core',
run: (context) => context.host.initializeCore(),
),
InitializationStep<AppHost>(
id: 'restoreSession',
dependsOn: const <String>{'core'},
run: (context) async {
final token = await context.host.restoreSession();
if (token != null) {
context.openGate(InitializationGate.authenticated, value: token);
}
},
),
InitializationStep<AppHost>(
id: 'guestContent',
dependsOn: const <String>{'core'},
run: (context) => context.host.preloadGuestContent(),
),
InitializationStep<AppHost>.afterAuthentication(
id: 'packages',
dependsOn: const <String>{'restoreSession'},
run: (context) async {
final token = context.requireAuthenticatedValue<String>();
await context.host.fetchPackages(token);
},
),
],
),
);
engine.snapshotStream.listen((snapshot) {
if (snapshot.isWaitingForGates) {
// Navigate to login if that is your app's next step.
}
});
await engine.start();You define one immutable InitializationPlan. Each step can:
- depend on other step ids
- wait for one or more gates
- skip itself with a
shouldRunpredicate - classify its own errors by severity
The engine runs steps in declaration order, respecting dependencies and gate
requirements. When no more steps can advance because of closed gates, the
lifecycle becomes waitingForGates. When a retryable or fatal failure blocks
progress, the lifecycle becomes blocked or failed and the failure details
are available through snapshot.lastFailure.
Gates let you split bootstrap into multiple phases without hard-coding the order. Both common auth scenarios use the same plan:
- Saved session restored during startup - the app opens
InitializationGate.authenticatedimmediately and post-auth steps run on the splash screen. - No saved session - the engine completes the non-auth work, reports
waitingForGates, and the host opens the gate later after login.
Create steps that wait for the built-in auth gate with the
InitializationStep.afterAuthentication factory, or define custom gates for
any deferred phase:
const featureFlags = InitializationGate<Map<String, bool>>('featureFlags');
InitializationStep<AppHost>.afterGate(
id: 'applyFlags',
gate: featureFlags,
run: (context) {
final flags = context.requireGateValue(featureFlags);
// Apply feature flags to the host.
},
);Every step error is routed through a severity resolver. The three built-in levels control what happens next:
| Severity | Engine reaction | Host action |
|---|---|---|
skippable |
Records the step as skipped, continues | None required |
retryable |
Pauses the run on blocked |
Call engine.retry() |
fatal |
Stops the run on failed |
Create a new engine |
Provide a custom resolver per step to map specific exceptions:
InitializationStep<AppHost>(
id: 'analytics',
run: (context) => initAnalytics(),
severity: (error, stackTrace) {
if (error is SocketException) return InitializationSeverity.skippable;
return InitializationSeverity.retryable;
},
);InitializationSnapshot is the detailed runtime view. It includes:
- lifecycle state and current step
- open gates and waiting gates
- counts for completed, pending, failed, skipped, and active work
- per-step state list with timestamps and failure details
- a
progressratio over currently active (non-deferred) work
InitializationSummary is the compact top-line view, available through
snapshot.summary or engine.summary. It exposes an
InitializationCompletionStatus and per-outcome counts, useful for logging,
analytics, or deciding how a run ended without inspecting every step.
Key snapshot helpers:
snapshot.isReadyForHandoff // completed or waiting on deferred gates
snapshot.isFullyComplete // every step done, no deferred work
snapshot.canRetry // blocked on a retryable failure
snapshot.progress // 0.0 to 1.0 over active work| Type | Purpose |
|---|---|
InitializationEngine<T> |
Runtime orchestrator |
InitializationPlan<T> |
Immutable plan definition |
InitializationStep<T> |
Step metadata and execution callback |
InitializationStepContext<T> |
Runtime helpers exposed to a running step |
InitializationSnapshot |
Immutable engine state at a point in time |
InitializationSummary |
Compact aggregate outcome |
InitializationFailure |
Structured failure details |
InitializationRecord |
Append-only journal entry |
InitializationGate<T> |
Typed named gate for deferred phases |
InitializationCancellationToken |
Cooperative cancellation token |
The example/ directory includes a Flutter app with two integrations over the
same engine:
- Riverpod 3 adapter
- BLoC / Cubit adapter
Both demos show launching with a saved token, launching without a token and opening the auth gate after login, and handling a retryable post-auth failure.
Run the package tests:
dart testGenerate a local line-coverage report and enforce a threshold:
dart test --coverage=coverage
dart run tool/check_coverage.dartThe checker defaults to a 99% line-coverage target. Use a custom threshold when needed:
dart run tool/check_coverage.dart --min-line=95If you want branch coverage too, generate it explicitly and set a branch target:
dart test --coverage=coverage --branch-coverage
dart run tool/check_coverage.dart --min-line=99 --min-branch=90MIT - see LICENSE for details.