DEV Community

BellSal
BellSal

Posted on

Shipping a Flutter arcade game to both app stores with AdMob and consent — 4 things that bit me

I built a small arcade puzzle game in Flutter and shipped it to Google Play and the App Store from one codebase. The gameplay was the easy part. Monetizing it with ads without making the game feel like a slot machine, and getting the privacy consent right, is where the real work hid. Here are the four things that actually broke.

1. The interstitial that showed up at the worst possible moment

The naive way to add an interstitial is: game over, show ad. Ship that and players hate you, because the ad fires the instant they die — before they've even processed the loss — and again on every single retry.

Two rules fixed it:

  • Never on the first game over. A new player who eats a fullscreen ad ten seconds in just uninstalls. Gate it behind a play counter.
  • Rate-limit it in wall-clock time, not by count. Track the timestamp of the last interstitial and refuse to show another within N minutes, regardless of how many times the player died.
DateTime? _lastInterstitial;
int _gameOvers = 0;

bool _canShowInterstitial() {
  _gameOvers++;
  if (_gameOvers < 3) return false; // let them settle in first
  final last = _lastInterstitial;
  if (last != null && DateTime.now().difference(last) < const Duration(minutes: 2)) {
    return false;
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

The counter and timestamp both have to persist, or a player who force-quits between rounds sees an ad every launch.

2. Consent has to run before the first ad request, not alongside it

If you serve ads in the EU/UK you need a consent flow (Google's User Messaging Platform, UMP). The mistake I made first: kicking off consent and ad initialization in parallel at startup. That's a race. Sometimes the first banner requested before consent resolved, which is exactly the thing the consent form exists to prevent.

The correct order is strictly sequential: request the consent info, load and show the form if required, and only after that initialize the Mobile Ads SDK and request anything.

final params = ConsentRequestParameters();
ConsentInformation.instance.requestConsentInfoUpdate(params, () async {
  if (await ConsentInformation.instance.isConsentFormAvailable()) {
    await _loadAndShowConsentFormIfRequired();
  }
  // ONLY now:
  await MobileAds.instance.initialize();
  _loadBanner();
}, (error) { /* fail open: init ads without personalization */ });
Enter fullscreen mode Exit fullscreen mode

Test it by forcing an EEA geography in the UMP debug settings, not just on your own device in your own country — otherwise you'll never see the form and think it works.

3. The rewarded video that closed early left players stuck

I used a rewarded ad to grant an extra life. The bug: if the user dismisses the ad early, the "reward earned" callback never fires, but my code was waiting on it to resume the game. Result: a dead screen.

The fix is to treat earned and closed as two independent events, and always have a path forward on dismissal:

bool earned = false;
ad.fullScreenContentCallback = FullScreenContentCallback(
  onAdDismissedFullScreenContent: (ad) {
    ad.dispose();
    earned ? _grantExtraLife() : _returnToGameOver(); // never just hang
  },
);
ad.show(onUserEarnedReward: (_, __) => earned = true);
Enter fullscreen mode Exit fullscreen mode

Race conditions around ad lifecycle callbacks are the single most common source of "the game froze" reports. Assume every callback might not fire.

4. Test ads in debug, real ads only in release — enforced by the build, not by memory

The classic disaster is shipping a build that still points at test ad unit IDs (zero revenue) — or worse, developing against your real units and getting flagged for invalid traffic by clicking your own ads.

Don't rely on remembering to swap IDs. Wire it to the build mode:

const bannerUnitId = kReleaseMode
  ? 'ca-app-pub-REAL/REAL'
  : 'ca-app-pub-3940256099942544/6300978111'; // Google's official test ID
Enter fullscreen mode Exit fullscreen mode

Debug and profile builds physically cannot serve real ads; release builds physically cannot serve test ads. It's one line and it removes an entire category of mistake.

The takeaway

None of this is about ad revenue being high — for a free game it rarely is. It's that the difference between a game people keep and one they uninstall in a day is almost entirely in when and how the ads appear, and whether the lifecycle callbacks are handled defensively. Get the pacing and the consent order right and the ads become almost invisible; get them wrong and no amount of good gameplay survives it.

The game this came from is GridZap — an arcade puzzle where you trace grid lines to close squares while enemies chase you (iOS here). Disclosure: it's mine, and every code pattern above is what actually ships in it. Zen of the whole thing: a banner, one interstitial between levels, and an optional rewarded video — nothing more.

Top comments (0)