◉CLAUDE LABJP
●2.1.310 — Monitor tool now works on Bedrock, Vertex and Foundry, and gateway failures are fixed (10/23)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 36 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…●2.1.310 — Monitor tool now works on Bedrock, Vertex and Foundry, and gateway failures are fixed (10/23)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 36 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-06-12Intermediate

Untangling Android Back-Button Ad Gates: A Parallel, Priority-Ordered Redesign with Claude Code

Nested back-button ad gates fired at the wrong moments. The parallel, priority-ordered redesign we shipped in v2.1.0, with Claude Code, Kotlin, and tests.

claude-code132android9admobkotlin3refactoring4

✦ Premium Article

In May 2026, while preparing v2.1.0 of the Android edition of my wallpaper app, two contradictory signals landed in the same week. Store reviews said the app showed an ad every single time they tried to leave. Meanwhile, the AdMob dashboard showed exit-interstitial impressions running at roughly half of what the frequency settings should produce. "Too many ads" and "no ads at all" were both true, for the same release, at the same time.

That combination told me the problem was not the ad SDK. It was my own branching logic. The culprit turned out to be the back-button handler: a stack of nested conditionals that had accumulated over nearly a decade of feature work. This is the record of how I rebuilt that nesting into a set of parallel, independent gates, and how I delegated the path inventory and test generation to Claude Code over roughly two weeks. If your app has an exit flow that nobody on the team wants to touch, the design and the migration order below should transfer directly.

The Symptom: One Exit Flow Producing Both "Never" and "Always"

The screen in question is the app's top-level screen. When the back button is pressed there, the app has to answer five questions.

  • Is this an ad-free user? Either a one-time ad-removal purchase, or a temporary ad-free window earned by watching a rewarded ad
  • Is another modal already visible? The exit ad must not stack on top of the review prompt or an announcement dialog
  • Should the review prompt take this slot? Based on launch counts and days since the last prompt
  • Has the frequency cap been reached? Exit interstitials have a minimum interval between impressions
  • Is the interstitial actually loaded? If not, there is nothing to show

Written as a list, this looks tidy. In the real code, those five concerns were buried inside nested if-statements, in whatever order each feature happened to be added. The two bug families mapped cleanly onto that structure. In one path, the review prompt's "showing" flag could get stuck and never come down — users who entered that state never saw an exit ad again, which produced the missing impressions on the AdMob side. In another path, the "ad not loaded" fallthrough skipped the frequency-cap bookkeeping entirely, so the next session showed an ad far earlier than the configured interval — which produced the angry reviews.

The Root Cause: Nesting Encodes Priority Implicitly

Here is the before state, simplified and renamed but structurally faithful.

// Before: the nested branching that had accumulated in the back handler
override fun onBackPressed() {
    if (!billingManager.isPurchased) {
        if (interstitialAd != null) {
            if (!reviewInduction.isShowing) {
                if (frequencyCap.canShow()) {
                    interstitialAd?.show(this)      // finally, the ad
                } else {
                    super.onBackPressed()           // cap reached -> fall through
                }
            }
            // when reviewInduction.isShowing == true: do nothing (bug nursery)
        } else {
            super.onBackPressed()                   // not loaded -> fall through, uncounted
        }
    } else {
        super.onBackPressed()                       // purchased -> fall through
    }
}

The real problem is not that this code contains two bugs. It is that the structure has three properties that keep producing bugs.

First, priority lives in the nesting depth, where it cannot be read as a specification. The business rules — "billing outranks the review prompt", "the review prompt outranks the ad" — exist only as the accidental order in which the ifs were written. When you add a new condition, the code gives you no basis for deciding which depth it belongs at.

Second, every added condition doubles the path count. Five booleans means 32 theoretical states, and every one of them deserves a deliberate answer. In a nested structure, the combinations you never wrote down fall into whatever else-branch happens to catch them. Whether they land on super.onBackPressed() or on "silently do nothing" is an accident of indentation.

Third, the fall-through paths are mute. When no ad appeared, the logs could not tell me whether the user was purchased, the ad was unloaded, or the cap was hit. That silence is exactly why every "the ad never shows" investigation used to take half a day.

I have been building apps as an indie developer since 2014, and I know perfectly well that "it works, so don't touch it" code is the most expensive kind. The exit flow still sat untouched for years because verifying it manually was so tedious. What finally got me moving was realizing I could hand the tedious part — enumerating the reachable paths — to Claude Code.

✦

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
✦You will be able to diagnose 'the ad never shows' and 'too many ads' reports in minutes, because every back-button decision is logged per gate
✦You can replace years of nested if-statements with a priority-ordered, first-match gate pipeline in Kotlin, following the 5 migration steps in this article
✦You will consolidate isAdFree || isRewardAdFree into a single source of truth, so billing state and rewarded-ad unlocks can never silently disagree
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

Claude 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

⟐ Claude Code2026-05-27
11 Days in Crashlytics: A Claude Code Debug Loop Across Two Android Wallpaper Apps
After shipping Beautiful Wallpapers v2.0.0 and Ukiyo-e Wallpapers v1.7.0 in early May, Crashlytics and Play Console threw more than 30 new issues at me in 11 days. This is the operations log of how I drove the fix list down to v2.1.1 / v1.8.1 using Claude Code as a triage partner.
⟐ Claude Code2026-08-28
A small script that catches invisible characters in plugin names before you install
Control characters and zero-width characters hiding in plugin or skill names are invisible in a terminal listing. I built six deliberately confusing names, compared what ls actually shows, and wrote a short check you can run before installing anything — plus an honest note on what that check misses.
⟐ Claude Code2026-08-26
I decide the prompt cache TTL by where I come back to, not how long I was away
Claude Code v2.1.242 added promptCacheTtl and subagentPromptCacheTtl. Whether the one-hour cache pays off depends on returning to the same directory and the same git state, not on how long the break was. Here is the order I check things in.