app-startup-and-bootstrap

Installation
SKILL.md

App startup and bootstrap

main() has one job: install a crash net, read the little state the first frame needs, wire real dependencies into the tree, and hand off to runApp — fast, ordered, and unable to hide a failure. Everything expensive happens after the first frame or off the launch path entirely.

Non-negotiable rules

  1. Error handlers go first, before anything that can throw. A crash-log sink, FlutterError.onError, and PlatformDispatcher.instance.onError are installed immediately after WidgetsFlutterBinding.ensureInitialized(). The step most likely to throw is opening the DB; installing handlers after it inverts the whole point.
  2. Exactly two error handlers — no zone. FlutterError.onError (build/layout/paint errors) and PlatformDispatcher.instance.onError (uncaught async errors) cover every path. Never add runZonedGuarded. The "you need all three" advice is crash-SDK advice (Sentry wraps its init in a zone); with no such SDK a zone buys nothing and costs a documented zone-mismatch footgun. Flutter's own fix for that warning is to remove zones.
  3. PlatformDispatcher.onError returns true unconditionally. Returning false routes to the embedder fallback, where the process may exit or hang. Get debug-console visibility from debugPrint under kDebugMode, not from return kReleaseMode.
  4. Never let an error handler throw. Wrap its body in a bare try/catch (_) and keep the comment explaining why the discarded error is deliberate — otherwise someone "fixes" it into infinite recursion inside the handler.
  5. Read settings/theme before runApp. Palette, text-scale policy, locale, and any first-paint choice are read synchronously (a handful of rows is sub-10ms) so frame one paints correct. A flash of the wrong theme is a visible defect, not a cosmetic one.
  6. Construct real infra in a composition-root bootstrap(), inject via overrides. Feature code depends on throwing placeholder providers; bootstrap() builds the real DB/services and overrideWithValues them in the root ProviderScope. A forgotten wiring fails loudly at startup, never returns null. This is also the test seam.
  7. Defer warm-up to addPostFrameCallback; never await it in main(). Any plugin/engine warm-up (audio, TTS, first network handshake) runs its cost synchronously on the main thread and produces ANRs on the cold-start path. Fire it best-effort after the first usable frame.
  8. Do not block the first frame. The only launch-path await is the one unavoidable blocker (opening the DB). Show the UI shell immediately rather than a blank window while a migration runs.
  9. Unwrap ProviderException before logging. Riverpod 3 rethrows provider failures wrapped; logging the wrapper hides the real cause and makes every entry read ProviderException.
  10. Tune Riverpod retry for the app's failure model. Riverpod 3 retries failing providers by default (~38s of exponential backoff). For a provider whose only failure is a local bug (corrupt DB, missing file), set retry: (count, error) => null so it fails immediately and loudly instead of spinning behind a spinner.
  11. Flush durable state on background via one lifecycle observer. Register a single WidgetsBindingObserver and, in didChangeAppLifecycleState, flush pending writes when the app reaches inactive/paused — the OS can kill a backgrounded app with no further callback — and re-read time-sensitive state on resumed. This is bootstrap's mirror image: bootstrap() restores state on cold launch, the observer persists it before the process can die. Read services in the callback via ref.read (never watch).

The sequence

Installs
73
GitHub Stars
34
First Seen
Aug 15, 2026
app-startup-and-bootstrap — zakariaf/flutter-skills