◉ANTIGRAVITY LABJP
Articles/App Development
▣ App Development/2026-07-14Advanced

Protecting Screenshot Tests for AdMob Screens from Ad Nondeterminism

Screens with ads turn red on every screenshot run, and eventually nobody reviews the diffs. Here is a design that seals off AdMob banner nondeterminism and leaves only real layout breaks in your checks, with Compose code and Antigravity-driven diff triage.

Antigravity386AdMob12screenshot testingJetpack Compose6Android27

✦ Premium Article

I keep a single AdMob banner on the settings screen of a wallpaper app. One night, trying to tidy up the screenshot tests for that screen, my hand stopped. Same code, same device configuration, same locale — and yet the test went red on every run. When I opened the diff viewer, the only thing that had changed was the ad creative inside the banner.

At first I told myself it was a fluke and decided to ignore it. But before long, even when a genuine layout break slipped in, I had stopped opening the diff viewer at all. A test that cries wolf every day is not believed on the day the wolf arrives. To protect a screen with an ad on it, you first have to push the ad itself out of what you check.

Why an ad-bearing screen turns red every time

A screenshot test compares rendered output against a baseline pixel by pixel. Introduce even one element the developer cannot control, and the test becomes fundamentally nondeterministic. An AdMob banner is the prime example.

The server decides the ad's contents on every request. Depending on load timing, the slot might be empty at capture, or drawn halfway. Even the rendered height can drift by a few pixels between creatives. The table below organizes the sources of nondeterminism hiding in an ad-bearing screen.

SourceSymptomHow to seal it
Creative contentsA completely different image or copy each runSwap the ad for a fixed placeholder during tests
Load timingEmpty, half, or complete changes the resultDo not load a real ad at all
Measured height driftA few pixels shift everything around itFix the slot height and verify layout as a contract
Network dependenceLoad fails in CI, making the result unstableDetach the test from the network

The point is singular. Do not try to "compare the ad well" — take it out of the comparison. Design outward from there.

Decouple the ad rendering policy from the code

What helped most was refusing to bake the decision of whether to draw an ad into the screen's code, and instead making it swappable from outside. In Compose, CompositionLocal was exactly the right tool. The screen only declares "there is an ad slot here," and the caller decides what actually gets drawn.

enum class AdRendering { LIVE, PLACEHOLDER }
 
// Default is live rendering. Only tests swap this out.
val LocalAdRendering = staticCompositionLocalOf { AdRendering.LIVE }
 
@Composable
fun AdBannerSlot(modifier: Modifier = Modifier) {
    when (LocalAdRendering.current) {
        AdRendering.PLACEHOLDER ->
            // A box with the same measured height as the real banner. Deterministic contents.
            Box(
                modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .background(Color(0xFFECECEC))
                    .testTag("ad_slot")
            )
 
        AdRendering.LIVE ->
            AndroidView(
                modifier = modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .testTag("ad_slot"),
                factory = { ctx ->
                    AdView(ctx).apply {
                        setAdSize(AdSize.BANNER)
                        // Google's official test banner ID. Inject the real one from production config.
                        adUnitId = "ca-app-pub-3940256099942544/6300978111"
                        loadAd(AdRequest.Builder().build())
                    }
                }
            )
    }
}

The screen knows an ad exists, yet has no involvement in its contents.

@Composable
fun SettingsScreen(state: SettingsState) {
    Column {
        SettingsHeader(title = "Settings")
        SettingsList(items = state.items)
        AdBannerSlot()               // LocalAdRendering decides what gets drawn
        SettingsFooter(version = state.version)
    }
}

With that one move, the test no longer waits for an ad, nor compares one. What gets drawn is always the same gray box.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A design that decouples the ad rendering policy via CompositionLocal to eliminate false positives from ad diffs, with a Compose implementation
✦When to drop pixel-exact matching in favor of layout-contract assertions, with concrete tests for height, clipping, and overlap
✦How to word the instruction that lets an Antigravity agent triage screenshot diffs while excluding the ad region
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

▣ App Dev2026-05-25
One Month Splitting Antigravity's Inline Edit and Agent Mode Across Four Wallpaper Apps
A month of notes from running Antigravity's Inline Edit and Agent Mode across four production wallpaper apps — with real counts, the decision rule I wrote into AGENTS.md, a one-pass dSYM fix, and how credit cost factors in.
▣ App Dev2026-05-21
Tuning AdMob Placements at Runtime with Firebase Remote Config and an Antigravity Agent
A practical look at how I combine Firebase Remote Config with an Antigravity Agent to nudge AdMob placements while the app is live, drawn from years of running wallpaper apps as an indie developer.
▣ App Dev2026-05-10
Three Decisions I Kept for Myself When Antigravity Maintained My Older Apps
How far could Antigravity maintain my long-running wallpaper and wellness apps? A field log of crash fixes and a staged rollout, with the lines I drew between delegated and retained work, and how I write context notes.