Free weekly email: the week's new posts, videos and one tip from the books, every Sunday. Get it free →
Homelab the Right Way

Home › Posts

Ads that don't get your app flagged: AdMob's interstitial rules in plain English

AdMob's "Disallowed interstitial implementations" page in short, plus a real frequency cap from a shipping app and the two-flag check every ad call should make.

· 3 min read · androidadmobmonetizationadskotlin

Ads that don't get your app flagged: AdMob's interstitial rules in plain English

An interstitial fires on app launch. Another fires when the user taps Back. A banner sits one finger-width from the main button. Each one looks harmless on its own. Together they produce accidental clicks, which AdMob treats as invalid traffic, and reviews that mention nothing but the ads.

This post covers what AdMob actually forbids for interstitials, how a small app turns that into a frequency cap in code, and the one check every ad call should make. It is drawn from Chapter 4 (Ad Placements That Don't Annoy) of Get Paid for Your Android App.

What AdMob forbids

AdMob's help page "Disallowed interstitial implementations" lists the patterns that get apps flagged. In short:

  • Not on app load or exit. Interstitials belong between pages of app content. For loading or returning to the app, Google recommends an app open ad instead.
  • Not after every action. The page says you should place no more than one interstitial ad after every two user actions, and that this includes tapping the Back button. No interstitial immediately after another one was closed.
  • Not in the way of navigation or the app's core content.
  • Not unexpectedly. An interstitial that appears while the user is focused on a task (playing, filling a form, reading) leads to accidental clicks. Show them only at logical breaks, and preload them so they do not pop up late, after the next screen has already appeared.

A real frequency cap

The book uses the author's TipsyBuddy app (a drink-logging app) as a worked example. Its InterstitialAdManager.kt keeps two caps and skips ads entirely for subscribers:

// From TipsyBuddy, ads/InterstitialAdManager.kt (Legacy SDK)
/** Minimum drinks logged between interstitial attempts. */
private const val MIN_ACTIONS_BETWEEN_SHOWS = 3
/** Minimum wall-clock gap between successful shows. */
private const val MIN_INTERVAL_MS = 90_000L

So an interstitial needs at least 3 logged actions and at least 90 seconds since the last one. Three actions is stricter than AdMob's two-action floor.

The file's header comment states the rule that matters most: it "fails silently if an ad cannot load" and "never blocks drink logging". An ad that is not ready is skipped, not waited for. The core action of the app always wins.

The same idea with the Next-Gen SDK

Here is the pattern written against the Next-Gen API from Google's "Load a single interstitial ad" guide. Imports from com.google.android.libraries.ads.mobile.sdk.* are omitted; Android Studio adds them.

class InterstitialController(
    private val adUnitId: String,
    private val isAdFree: () -> Boolean,
) {
    @Volatile private var ad: InterstitialAd? = null
    @Volatile private var loading = false
    private var actions = 0
    private var lastShownAt = 0L

    fun preload() {
        if (isAdFree() || ad != null || loading) return
        loading = true
        InterstitialAd.load(
            AdRequest.Builder(adUnitId).build(),
            object : AdLoadCallback<InterstitialAd> {
                override fun onAdLoaded(ad: InterstitialAd) {
                    this@InterstitialController.ad = ad; loading = false
                }
                override fun onAdFailedToLoad(adError: LoadAdError) {
                    loading = false   // try again at the next natural break
                }
            },
        )
    }

    /** Call at a natural break, e.g. after an item is saved. Main thread. */
    fun onNaturalBreak(activity: Activity) {
        if (isAdFree()) return
        actions++
        val ready = ad
        val now = SystemClock.elapsedRealtime()
        val allowed = actions >= MIN_ACTIONS &&
            now - lastShownAt >= MIN_INTERVAL_MS
        if (ready == null || !allowed) { preload(); return }
        ready.adEventCallback = object : InterstitialAdEventCallback {
            override fun onAdDismissedFullScreenContent() { preload() }
            override fun onAdFailedToShowFullScreenContent(
                fullScreenContentError: FullScreenContentError
            ) { preload() }
        }
        ad = null
        actions = 0
        lastShownAt = now
        ready.show(activity)
    }

    companion object {
        const val MIN_ACTIONS = 3   // stricter than AdMob's two-action floor
        const val MIN_INTERVAL_MS = 90_000L
    }
}

What this gets right:

  1. It preloads. The ad is fetched ahead of time, so it is ready at the break instead of arriving late.
  2. It only shows at a natural break. You call onNaturalBreak() from the place in your UI where a unit of work ends, such as after an item is saved. Never on launch, never on exit.
  3. It counts actions and time. Both caps must pass.
  4. It never waits. If no ad is loaded, the moment passes and a new one is preloaded for next time.
  5. It respects paying users. isAdFree() short-circuits both loading and showing.

Call preload() once ads are ready, which in the book means after consent has been gathered and the SDK is initialized. Next-Gen also offers an ad preloading API (InterstitialAdPreloader) that keeps a cache filled for you. At the time of writing Google's guide labels it beta.

Every ad call checks the same two flags

Two rules sit above every placement decision. A user who paid to remove ads must never see one again. An app without consent must never request one. The book puts both checks in one place:

val showAds: Boolean get() = adsReady.value && !entitlements.adFree.value

adsReady covers consent and SDK setup. entitlements.adFree covers the remove-ads purchase. Every banner, interstitial and native ad reads showAds instead of making its own judgment. When one of those two facts changes, every ad in the app follows.

Checklist

  • No interstitial on app load or exit. App open ads are the format designed for cold start and returning to the app.
  • No more than one interstitial per two user actions, and Back counts as an action.
  • Never show one interstitial right after another was closed.
  • Show interstitials only at a natural break, never mid-task.
  • Preload, and skip silently when no ad is ready. Never block the core action.
  • Cap by actions and by time (TipsyBuddy uses 3 actions and 90 seconds).
  • Route every ad through one flag that checks consent and the remove-ads entitlement.

More articles