While preparing the v2.1.0 update for Beautiful HD Wallpapers — one of my apps with over 50 million cumulative downloads — I found a bug where pressing the back button in a specific sequence wouldn't trigger the interstitial ad. Tracing through the logs revealed the cause quickly, but fixing it took longer than expected.
The onBackPressed logic had nested if statements: one checking whether the user was ad-free, and another inside it checking whether it was the right timing to show an ad. Fixing one case broke another. It's a pattern I've hit a few times over 12 years of indie app development, and once I recognized it in Rork-generated code too, I decided to write up both the diagnosis and the solution.
The Problem with Nested Ad Gate Logic
Here's a simplified version of the problematic code:
// ❌ Nested structure (problematic)
override fun onBackPressed() {
if (isAdFree) {
super.onBackPressed()
} else {
if (shouldShowAd()) {
showInterstitial {
super.onBackPressed()
}
} else {
super.onBackPressed()
}
}
}At first glance it looks reasonable. But it doesn't distinguish between "ad-free because the user paid" and "ad-free because they watched a rewarded ad." When I added rewarded-ad exemption logic in v2.1.0, it wasn't clear which if block it should live in. That ambiguity is exactly where the bug came from.
The deeper issue with nested structures is that priority becomes implicit. Reading the code requires mentally expanding the full branch tree, and each new condition adds to that cognitive cost. In solo development — where you're the only code reviewer — this becomes a real maintenance burden over time.
Rewriting with a Parallel Independent Structure
Here's the revised version:
// ✅ Parallel structure (improved)
override fun onBackPressed() {
when {
isAdFree -> {
// User is premium or has watched a rewarded ad — go back without showing an ad
super.onBackPressed()
return
}
shouldShowAd() -> {
// It's time to show an interstitial — show it, then navigate back
showInterstitial { super.onBackPressed() }
return
}
else -> {
// All other cases (e.g., during cooldown) — just navigate back
super.onBackPressed()
}
}
}With a when block, each case is independently readable. Adding a new condition — like skipping ads on a specific screen, or using a debug flag to bypass ads — doesn't require touching existing branches.
Each branch also has an explicit return, making it clear that the case terminates there. In the nested version, the control flow was implicit. Making it explicit has a small but meaningful effect: when you come back to this code three months later, you understand the intent without having to re-trace the logic.
Centralizing ad-free State into a Single Source of Truth
The second root cause of the bug was that isAdFree was evaluated in multiple places. The billing state (from BillingManager) and the rewarded-ad state (from AdFreeManager) were referenced separately across the codebase, so when one changed, the other didn't always sync.
The fix was to centralize the judgment:
// AdFreeManager — single source of truth for ad-free state
class AdFreeManager(
private val billingManager: BillingManager,
private val rewardAdManager: RewardAdManager
) {
val isAdFree: Boolean
get() = billingManager.isPremiumUser || rewardAdManager.isRewardAdActive
val shouldShowAd: Boolean
get() = !isAdFree
}Now onBackPressed only references adFreeManager.isAdFree. Whether the billing state or the reward state changes, updating this class propagates everywhere. Code that evaluates the same boolean in multiple places will eventually diverge — this is especially risky for ad-free state, which is directly tied to the user's payment experience.
How to Get Rork to Generate This Pattern
When you use Rork to build an Android app with AdMob, the ad gate logic sometimes comes out nested, especially when you add "skip ads for paying users" after the initial generation. The AI tends to add the new condition inside an existing if block rather than restructuring the whole thing.
When that happens, give Rork an explicit structural instruction:
"Please rewrite the ad gate logic using a when block with parallel branches.
Use three independent cases: isAdFree / shouldShowAd / else.
Add an explicit return at the end of each case.
Centralize the isAdFree judgment into an AdFreeManager class
that combines BillingManager and RewardAdManager."
Naming the construct (when block), the case names, and the target class name gives Rork enough context to generate the right structure. I applied this approach when working on the Android version of Ukiyo-e Wallpapers — after receiving the initial Rork output, I sent this instruction for a regeneration and received code that required no further structural changes.
What Changed After the v2.1.0 Fix
Since applying this fix, ad-related bug reports from Beautiful HD Wallpapers have dropped to zero. The occasional "ads not showing" or "too many ads" reports that appeared once or twice a month have stopped. Customer support overhead from ad logic bugs is effectively gone.
Code review has also improved, even though I'm the sole reviewer. Reading the when block from top to bottom is all it takes to verify the ad gate logic. When adding new features that touch ad behavior, I can confirm the existing cases are unaffected without mentally rebuilding the nested structure.
For solo developers maintaining apps with ad-supported models, "future-you can read this quickly" is the most useful quality criterion for code. The parallel structure meets that bar clearly.
If you have ad gate logic in your app, run grep -r "if.*isAdFree" src/ or grep -r "isAdFree" app/ first. Any nested pattern you find is worth an hour of refactoring today to avoid hours of debugging later.