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.

Antigravity330AdMob12screenshot 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-06-26
Green in the Embedded Emulator, Broken on the First Real Device — Putting a Parity Gate on AI-Generated Compose Apps
AI Studio now generates Kotlin/Compose apps from a prompt, runs them in an embedded emulator, and pushes them to a real device over USB — all from one screen. Yet a screen that passed in the emulator can break the first time it lands on a real phone. As a solo developer running several apps, here is how I put a gate that catches device parity issues before they ship.
App Dev2026-06-21
A Few Low-Density Phones Lost Their Bundled Wallpaper — The drawable vs nodpi Boundary in Play's Density Splits
App Bundle density splits will happily split images that should never be split, dropping a static resource on one density bucket only. Here is how I reproduced it with bundletool and fixed it by moving to drawable-nodpi or disabling density split — with the decision criteria.
App Dev2026-07-06
Building a Live Wallpaper That Doesn't Drain the Battery — Designing the WallpaperService Render Loop Around a Power Budget
Live wallpapers quietly burn battery because they keep drawing when nothing is visible. Here is the WallpaperService.Engine design I used to cut power draw by roughly 60% in a real wallpaper app, built around visibility, idle, and thermal gates.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →