I was getting a Flutter app ready for production and built it a proper QA layer: golden screen tests at five popular phone sizes (all green), a clean flutter analyze, and a set of scripted device flows for the main user journeys. Everything I could point at said "done."
Then I launched it on an emulator for the first time — and got a white screen. Not a slow one. I screenshotted at 3, 10, and 20 seconds: still blank white, forever.
None of my tests had caught it, and once I saw why, it was obvious. Golden and widget tests mount individual screens directly — they never run the app's real main(). flutter analyze is static. My device flows were written but hadn't actually been run against a cold launch yet. Every check I had skipped the one line that was broken.
logcat told the story: a subscription SDK was answering 401 and retrying on a loop. And main() did await sdk.start() before runApp(). With the SDK slow and rejecting, start() never returned, so runApp() was never called — no first frame, just the platform's white window.
The fix was to stop letting a network-dependent SDK gate the first frame: memoize its start() future, fire it without awaiting at launch, and let the few consumers that actually need it (await the same memoized future internally). The UI renders immediately and reacts as the SDK resolves.
The lesson I keep relearning: your whole test suite can be green and the app still won't open. Run the real thing on a real device as the last QA step — especially with its slow dependency failing on purpose.
Reproduce this
Prerequisites: a Flutter app whose main() does async initialization before runApp() — a purchases/subscription SDK (e.g. Apphud), remote config, an auth handshake, anything that touches the network. An emulator or device. Your agent will need from you: the app, and the ability to make one of those startup dependencies fail (airplane mode, a wrong API key, or a blocked host). Steps:
- Open
lib/main.dartand find everyawaitbetweenWidgetsFlutterBinding.ensureInitialized()andrunApp(...). Flag any that hit the network or an SDK that can hang or reject. - Launch the app cold with that dependency unreachable or rejecting. Capture a screenshot several seconds in — a persistent white screen confirms the first frame is blocked. On Android,
adb logcatwill show the offending SDK retrying. - Refactor: move the blocking
awaitout of the pre-runApppath. Memoize the init as aFutureyou start (but don't await) beforerunApp, and have only the consumers that need it await that same future.
Verify: launch cold again with the dependency still failing. The splash/first screen must render within about a second instead of a white screen — the app is usable while the background init is still resolving or failing.