ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-06-21Advanced

The Back Button Showed an Interstitial Sometimes, Not Others — Rewriting Nested ifs Into a List of Independent Guards

Interstitial display on back press was unstable because nested if statements hid the priority between conditions. Here is how I split it into reason-returning guards and generated tests from a decision table.

antigravity385admob14android21app-dev34agent15

Premium Article

Right after a user finishes a rewarded video, they sometimes press the back button — and very occasionally an interstitial fires immediately afterward. I learned about this in one of the wallpaper apps I run as a solo developer, not from Crashlytics but from a user review. The reproduction rate was low, and I could never trigger it on my own device.

The cause was not a crash. It was the logic that decided whether to show an ad on back press. Four conditions — frequency cap, cooldown, whether the user had paid, and whether an ad was loaded — were written as nested if statements, and which condition took priority appeared only implicitly in the structure of the code. This is a record of how I reworked that decision into an ordered list of independent guards that each return a reason, generated tests from a decision table, and stopped the regression.

Nested ifs hide priority inside their structure

The problem code looked roughly like this.

fun onBackPressed() {
    if (!billing.isAdFree) {
        if (interstitial.isLoaded) {
            if (System.currentTimeMillis() - lastShownAt > COOLDOWN_MS) {
                if (backPressCount % SHOW_EVERY == 0) {
                    interstitial.show(activity)
                    lastShownAt = System.currentTimeMillis()
                }
            }
        }
    }
    super.onBackPressed()
}

At a glance it looks correct, and most of the time it behaves correctly. The trouble was the state right after a rewarded video. There was a path where the rewarded view forgot to update lastShownAt, so the cooldown check could slip through. On top of that, backPressCount was also incremented on other screens, mixing in counts from operations that were not back presses.

In a nested structure, the relationships between conditions are invisible. Whether cooldown or the frequency cap is evaluated first, whether the ad-free check is truly the highest priority — you only know by reading the code line by line. Four conditions are still manageable, but once consent state (UMP) and in-app review prompts start competing for the same moment, the nesting quickly becomes impossible to follow.

Decompose the decision into reason-returning guards

The rework was simple in principle. Instead of expressing "may we show this" as one large boolean, I turned it into a list of small guards that each independently return a block reason. If any single guard blocks, we do not show. Only when every guard passes do we show.

sealed interface AdDecision {
    data object Allow : AdDecision
    data class Block(val reason: String) : AdDecision
}
 
// Each guard returns the reason it blocks, or null if it passes.
private val guards: List<(AdContext) -> String?> = listOf(
    { ctx -> if (ctx.isAdFree) "ad_free" else null },
    { ctx -> if (!ctx.isLoaded) "not_loaded" else null },
    { ctx -> if (ctx.sinceLastShownMs < COOLDOWN_MS) "cooldown" else null },
    { ctx -> if (ctx.backPressCount % SHOW_EVERY != 0) "frequency" else null },
)
 
fun decideOnBackPress(ctx: AdContext): AdDecision {
    val reason = guards.firstNotNullOfOrNull { it(ctx) }
    return if (reason == null) AdDecision.Allow else AdDecision.Block(reason)
}

The call site can then cleanly separate the decision from the action.

fun onBackPressed() {
    when (val d = decideOnBackPress(adContext())) {
        is AdDecision.Allow -> {
            interstitial.show(activity)
            lastShownAt = clock.now()
        }
        is AdDecision.Block -> log.d("interstitial blocked: ${d.reason}")
    }
    super.onBackPressed()
}

What I appreciated most about this shape is that the block reason ends up in the logs. Before, all I knew was that "it didn't show." Now cooldown versus frequency is always recorded. A low-repro bug became something I could chase in logs rather than in reviews.

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
Reworking the back-press interstitial decision from nested ifs into an ordered list of reason-returning guards
A concrete way to start from a decision table and have Antigravity generate the parameterized tests
Criteria for keeping ad-free state, cooldown, frequency cap, and load readiness as separate responsibilities
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-13
Implementing Google ML Kit with Antigravity: What the Docs Don't Tell You
A practical guide to integrating Google ML Kit into iOS and Android apps with Antigravity. Covers text recognition, face detection, the Xcode 15 SPM bug workaround, and honest notes on where AI assistance helps — and doesn't.
App Dev2026-04-25
Making Sense of Play Console Data with Antigravity — A Practical Guide for Indie Developers
Stop just staring at Play Console dashboards. Learn how to feed your review data, crash reports, and revenue metrics into Antigravity to get concrete improvement actions — a practical guide for solo Android developers.
App Dev2026-04-03
Antigravity × Capacitor Hybrid App Implementation Guide: An AI-Driven Workflow for Taking a Web App to iOS/Android Native
Practical implementation notes for porting an existing web app to iOS/Android native with Capacitor and Antigravity, grounded in lessons from years of running indie apps in production. Covers native API integration, AdMob tuning, and App Store / Google Play submission with measured numbers throughout.
📚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 →