Journal

Never wait before the first frame

Google Play rejected one of the calculator apps as unresponsive. The screenshot attached was the launch splash. On every emulator the app came up in a second.

1 min#android#flutter#play#troubleshooting

The email subject was "Action required: your app does not comply with Google Play policies". The body said "App not responding" and carried one screenshot. It showed the launch splash, the icon and nothing else. Flutter's first frame never got drawn.

Where the details are

The console's policy page shows "Broken functionality policy — rejected" and a date. Which screen, and how it froze, is only in the email attachment. Asking the API after resubmitting returns IN_REVIEW. A rejection has to be read from the mail.

The cause

The main() shared by 99 apps looked like this.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initAds();        // MobileAds.initialize + gate
  await billing.init();   // Play Billing connection
  runApp(const App());
}

On an emulator, online or offline, the app is up in a second. Review devices are different: WebView disabled, no Play account, no GMS ads module. There, MobileAds.initialize or the Billing connection either holds the platform thread or throws, and both sit before runApp. The first frame never comes.

We could not reproduce it. The structure was wrong regardless.

The fix

In the shared package. The app-side main() files did not change, because the awaits now return at once.

  • initAds() returns immediately. The gate and MobileAds.initialize run after addPostFrameCallback and then complete adsReady.
  • Banner, interstitial, rewarded and app-open load() wait for adsReady before reading a unit id. Read it before the gate and production ships test units.
  • Billing.init() connects after the first frame too, with a ten-second cap on isAvailable. Product queries and restores run in the background, and tiles fill from a productsLoaded notifier.

One rule: nothing before the first frame waits on the network or a platform SDK. The template for new apps carries it.

What verification taught

Judging "the splash is over" by the share of white pixels failed: a cream-coloured UI scores 0.91 and reads as splash. A print('PROBE …') inside addPostFrameCallback marks the first frame instead.

Safe build, emulator online: banner after the first frame. Offline: UI within five seconds. Thirty-three apps were rebuilt and resubmitted that night.