Skip to content

App startup, from first principles

A six-part walkthrough of what happens between tapping the icon and seeing a film poster — why that used to take so long (and sometimes never finished at all), and what the current design does instead.

Written for someone who has not worked on this codebase’s startup path before. Each doc builds on the last, and every claim points at a real file.

Read them in order the first time through:

  1. The old startup — the sequence as it was: five network calls in single file, no deadline, nothing cached, and a 2.9 MB animation decoding on top of it. This is the “why”.
  2. The network layer — why a fresh Dio() per request was the single most expensive line in the app, what a missing connectTimeout actually does on a bad network, and the shared client that replaced it.
  3. Cache-first boot — the disk cache, how it is keyed, and the repository pattern that lets a screen paint from disk and upgrade to fresh data behind the user.
  4. The bootstrap and the splash — the three startup phases, what each is allowed to block, how the splash became a router decision instead of a loader, and why the profile gate may only ever be asked once per launch.
  5. The first frame and after — what still runs before runApp, what moved behind it, why four screens used to boot at once, and the two native launch screens nobody had touched.
  6. Before and after — the side-by-side comparison, how to measure it yourself, and what is deliberately still open.

Startup used to be a chain: the splash awaited /v2/settings, then a DNS probe, then /user/profiles, then the home feed, then continue-watching — each waiting on the one before it, each on a brand-new TCP connection, with no connectTimeout and nothing cached anywhere. On a healthy network that is five round trips; on a dead one it is the OS socket timeout five times over, which is why the app could sit on the looping splash GIF apparently forever. It now works the other way round: a returning user’s session, settings, nav items and home feed are restored from disk, the app routes straight to home, and the network refresh runs behind the user and swaps fresh content in when it lands. Only a genuine first install waits on the network, and that wait is parallel and capped.

main, as of August 2026, after the cache-first startup work. Everything described here is implemented — where something was deliberately left undone, doc 6 says so explicitly.

Doc Primary files
1. The old startup (history) lib/presentation/bloc/splash_screen/splash_screen_bloc.dart, lib/main.dart
2. The network layer lib/core/network/api_client.dart, lib/core/network/dio_api_provider.dart, lib/core/network/api_result.dart
3. Cache-first boot lib/data/datasources/local/api_cache_store.dart, lib/data/datasources/local/{home,content}_local_data_source.dart, lib/data/repositories/{home,content}_repository_impl.dart
4. Bootstrap & splash lib/domain/services/app_bootstrap.dart, lib/data/services/app_bootstrap_impl.dart, lib/presentation/bloc/splash_screen/splash_screen_bloc.dart, lib/presentation/pages/splash_screen.dart
5. First frame & after lib/main.dart, lib/presentation/pages/home_pages/main_scaffold.dart, lib/presentation/bloc/home_screen/home_screen_bloc.dart, android/app/src/main/res/drawable*/launch_background.xml, ios/Runner/Base.lproj/LaunchScreen.storyboard
6. Before and after all of the above, plus test/data/datasources/local/api_cache_store_test.dart, test/data/repositories/home_repository_impl_test.dart, test/presentation/bloc/splash_screen/splash_screen_routing_test.dart
  • ../playback/ — how a film actually plays once you have got this far.
  • ../api/README.md — the REST surface these startup calls are drawn from.