Skip to content

6. Before and after

══ BEFORE ═══════════════════════════════════════════════════════════════
tap native splash pre-runApp native work splash GIF (looping)
│ (WHITE flash) up to 21 s of timeout budget │
│ │ │ │
▼────────▼────────────────────▼────────────────────▼───────────────────
│
wait for ConnectivityCubit ────────────────────┤
│
① GET /v2/settings new TCP+TLS ──────┤
② InternetConnectionChecker DNS probes ──────┤ all serial
③ GET /user/profiles new TCP+TLS ──────┤ no connectTimeout
④ GET /v2/ (home feed) new TCP+TLS ──────┤ nothing cached
⑤ GET /continue-watching new TCP+TLS ──────┤ no deadline
│
route → /main_scaffold │
│
⑥ GET /v2/popup-notifications ────────────────┤ blank black screen
│
…and at this exact moment: 100 VideoPlayerControllers,
5 video downloads, 2 more API calls from offstage tabs
▼
FIRST POSTER
dead network → no cached content, no error screen (emit threw) → hang
══ AFTER ════════════════════════════════════════════════════════════════
RETURNING USER
tap native splash (BLACK) Hive + prefs route
│ │ │ │
▼────────▼───────────────────────▼──────────────▼
0 ms ~50 ms ~0.4 s → FIRST POSTER
(from disk)
│
└── behind the user, in parallel:
geo · settings · home · profiles
· continue-watching
each ≤10 s, set ≤12 s
│
▼
home_feed_revision++
→ fresh sections swap in
FIRST INSTALL
tap native splash Hive + prefs (miss) parallel fetch, ≤12 s route
│ │ │ │ │
▼────────▼────────────────▼────────────────────────▼───────────────▼
FIRST POSTER
dead network → cached feed if any; otherwise the retry screen,
which now actually renders
Before After
Blocking round trips to first poster 6 (5 serial + 1 on home) 0 signed out · 1 signed in (the profile gate) · 1 parallel batch on first install
TCP/TLS handshakes at startup one per request (~6) 1, reused (HTTP/2 multiplexed)
connectTimeout unset → OS default (75 s+ on iOS), per call 8 s
Overall startup deadline none 10 s per call, 12 s for the whole set
API response cache none (CacheOptions written, never attached) Hive api_cache, home feed 15 min · content 6 h
Offline second launch no-internet dialog lands on the cached home feed
Cached settings/nav persisted, effectively never read restored on every cold start
Pre-runApp native budget up to 21 s + a permission dialog Hive only; downloads init moved after first frame
Tabs built on reaching home 4 (+100 players, +5 video downloads) 1
Home first paint behind /v2/popup-notifications immediate, popup fetched after
Splash GIF decode 250 frames at 512×512 250 frames at display size
Native launch screen white flash both platforms black, seamless
Retry screen on failure unreachable (emit after handler completed) works

These were found while tracing the startup path; each is a real defect, not just a slow path.

  1. emit after handler completion (checkUpdate) — threw StateError, making the “Something went wrong / Try again” screen unreachable on the main failure path. Fixed by making the version check a synchronous predicate.
  2. getIt<List<NavItem>>() served a permanently stale list. GetIt caches the first resolved instance; navItemListGlobal is reassigned on every refresh. Any tab-bar change was invisible until the app was killed.
  3. VPN branch never set initial = true, so every connectivity event while on a VPN re-ran the entire startup sequence.
  4. checkGeoLocation() threw from an unawaited future — an unhandled async error on every failed geo lookup — while country (a query param on every URL) silently stayed at the hardcoded 'BD'.
  5. A failed popup-notification fetch blanked the home screen via emit(HomeScreenError(...)).
  6. Multi-profile accounts were trapped on the profile picker — found after the first cut of this work shipped, and worth recording because the cause is subtle. Unifying every route through one _routeOnward meant the profile gate was re-applied after the user picked a profile, and the gate reads user_profiles, which a switch does not shorten. Fixed by splitting cold-start routing (_routeOnward, gate applied) from post-selection routing (_routeAfterGate, gate skipped). Doc 4 covers it in full.
  7. hasReachedDeviceLimit() raced its own data. getCurrentDeviceCount() was fired unawaited and read a few lines later. Now in Phase C, and the gate reads restored state.

1. Read the phase timings. _quietly logs a label and elapsed ms for every startup step:

adb logcat | grep -E '\[boot\]|\[cache\]|\[home\]'

Record a baseline on the previous build and compare on the same device and account — the absolute numbers vary far too much between devices to be worth quoting.

2. Airplane mode, second launch. The headline behaviour. Launch once with network to populate the cache, kill the app, enable airplane mode, launch again. It must land on a populated home screen. Previously this showed the no-internet dialog.

3. Network Link Conditioner → “Very Bad Network” (iOS) or a throttled proxy. Must reach home within the budget and never hang. Also confirm the retry screen renders — it used to throw instead.

4. Fresh install, good network. Time to first poster, and confirm no notification-permission dialog appears before the first frame on Android 13+.

5. Logout and profile switch. Confirm api_cache is cleared and the next user never sees the previous one’s feed. There are unit tests for the scoping (test/data/datasources/local/api_cache_store_test.dart), but the end-to-end path is worth checking by hand.

6. Connection reuse. Proxy the app through Charles or Proxyman on a cold start and confirm the startup calls share one connection instead of opening five.

7. The suites.

Terminal window
fvm flutter analyze # 0 errors
fvm flutter test # 381 tests

New coverage: test/data/datasources/local/api_cache_store_test.dart (round-trip, TTL via the ambient clock, per-account and per-profile scoping, eviction, corruption, box-unavailable) and test/data/repositories/home_repository_impl_test.dart (cache-only reads, stale fallback, CacheFailure vs ServerFailure), and test/presentation/bloc/splash_screen/splash_screen_routing_test.dart (the startup destination decision: profile gate, force-update gate, device-limit gate, and their precedence).

8. Multi-profile accounts, by hand. Cold start → pick an unlocked profile → must land on home. Repeat with a locked profile through the PIN dialog, which reaches the same reload by a different route. Then switch profiles from inside the app. See doc 4 for why this is the one path most likely to break.

  • Cache scope is home feed + content pages only. Search, collections, artist pages, shorts and the subscription catalogue still go to the network every time. They are not on the startup path, so they were left out on purpose.
  • HTTP/2 is on for every verb, where before it was only on for PUT/DELETE. It is the highest-behavioural-risk change in this work. ApiClient._useHttp2 is a single const bool — flip it if the CDN misbehaves. The shared-connection win survives either way, since a shared Dio pools keep-alive connections regardless.
  • The splash GIF is still 2.9 MB on disk. cacheWidth bounds the decode, not the asset. Re-encoding it — or moving to Lottie, already a dependency — needs a new asset from design.
  • ShortsScreen.initState still allocates 100 VideoPlayerControllers and pre-downloads five videos. That is now deferred until the tab is opened rather than fixed; it deserves its own look.
  • ApiResult._reformat still fires UnauthorizeEvent on any 401, including one from a background revalidation. Worth guarding so a failed refresh cannot log a user out mid-session.
  • api_static vs api_dynamic is a natural cache boundary already present in the URL structure. Nothing exploits it yet.
  • The splash’s own retry paths skip the profile gate. The no-internet retry and the reconnect handler (splash_screen.dart:52, :147) dispatch SplashScreenReload, which by design does not gate — so a user who hits the offline dialog on the splash and then retries goes straight to home. This predates the cache-first work and is unchanged by it. Closing it would mean distinguishing the two reload sources, e.g. SplashScreenReload({bool postProfileGate = false}).