Beautiful HD Wallpapers の v2.1.0 を準備していたとき、特定の操作手順でだけ戻るボタンを押しても広告が表示されないバグに気づきました。コードを追うと原因はすぐ分かりましたが、修正に手間取りました。onBackPressed の中で「広告フリーかどうか」と「表示タイミングかどうか」の判定が入れ子になっていて、片方を直すともう一方のケースが壊れる状態になっていたからです。
累計5,000万DL超のアプリを個人で運営してきた中で何度か踏んできた罠ですが、同じパターンがRorkで生成したコードにも出やすいことに気づいてから、設計方針を明文化しました。このパターンは再現しやすく、かつ原因が分かりにくいため、備忘も兼ねて記録しておきます。
入れ子のad gateが引き起こす問題
問題のあったコードを簡略化するとこんな形です。
// ❌ 入れ子構造(問題あり)
override fun onBackPressed() {
if (isAdFree) {
super.onBackPressed()
} else {
if (shouldShowAd()) {
showInterstitial {
super.onBackPressed()
}
} else {
super.onBackPressed()
}
}
}一見シンプルですが、後から「報酬広告を視聴してad-freeになった場合」を追加しようとすると、どの if ブロックの内側に入れるかが曖昧になります。isAdFree が true のケースにまとめるべきか、あるいは shouldShowAd() の手前に割り込ませるべきか、コードを書いている側は判断できますが、読み返したときには分かりにくくなっています。
入れ子構造の根本的な問題は、条件の優先順位が暗黙になることです。分岐を追う際に全体のツリーを頭の中で展開する必要があり、新しい条件を追加するたびにその認知コストが増えます。Beautiful HD Wallpapers の場合、v2.0.0 から v2.1.0 の更新で「報酬広告視聴後の免除」ロジックを追加したときに、既存の入れ子に差し込む位置を一箇所間違えて、特定の遷移フローだけ広告が出ないバグが発生しました。
並列独立構造への書き直し
修正したコードはこうなりました。
// ✅ 並列独立構造(改善後)
override fun onBackPressed() {
when {
isAdFree -> {
// 課金または報酬視聴済み → 広告なしで戻る
super.onBackPressed()
return
}
shouldShowAd() -> {
// 広告表示タイミング → インタースティシャル表示後に戻る
showInterstitial { super.onBackPressed() }
return
}
else -> {
// それ以外(クールダウン中など)→ そのまま戻る
super.onBackPressed()
}
}
}when ブロックで条件を横並びにすると、各ケースが独立して読めます。新しい条件(「特定の画面では広告を出さない」「デバッグビルドでは広告をスキップ」)を追加する際も、既存の分岐に触れずに追記できます。
各ブランチに return を明示しているのがポイントです。入れ子のときは return の位置が暗黙でしたが、並列にすることで「このケースはここで完結する」という意図が明確になります。コードレビューや3ヶ月後の自分が読んだときに、意図が伝わるかどうかを個人開発では特に重視しています。チームのレビューアがいない分、未来の自分が唯一の読者になるからです。
ad-free状態のSource of Truthを一箇所に集める
入れ子バグの第二の根本原因は、isAdFree の判定がコード内の複数箇所に散らばっていたことです。課金状態(BillingManager)と報酬広告の視聴状態(AdFreeManager)を別々に参照していたため、一方が更新されたときに同期漏れが起きました。
修正後は、判定ロジックを一箇所に集約しました。
// AdFreeManager — 広告フリー判定のSource of Truth
class AdFreeManager(
private val billingManager: BillingManager,
private val rewardAdManager: RewardAdManager
) {
val isAdFree: Boolean
get() = billingManager.isPremiumUser || rewardAdManager.isRewardAdActive
val shouldShowAd: Boolean
get() = !isAdFree
}onBackPressed からは adFreeManager.isAdFree だけを参照します。課金状態と報酬状態のどちらが変わっても、このクラスだけを更新すれば全体に反映されます。個人開発を12年続けてきて実感するのは、「同じことを複数の場所で判定しているコード」は必ずある時点でずれる、ということです。ad-free の判定はユーザーの支払い体験に直結するため、Source of Truth を一箇所にまとめることが特に重要です。
RorkとAIアシスタントへの依頼の仕方
Rorkで AdMob を組み込んだアプリを生成すると、広告の表示ロジックが入れ子になって出てくることがあります。特に「課金ユーザーは広告を出さない」という要件を後から追加したとき、AIが既存の if ブロックの内側にネストする形で条件を追加しやすい傾向があります。
生成されたコードを受け取った後に自分で書き直すか、Rorkに対して構造を具体的に指定します。
「広告ゲートのロジックを並列構造(when ブロック)で書き直してください。
isAdFree / shouldShowAd / else の3ケースを独立させ、
各ブランチに return を明示してください。
ad-free判定は BillingManager と RewardAdManager を合成した
AdFreeManager クラスに一元化してください」
「入れ子を避けて」だけでは判断が難しいため、when ブロックという具体的な構文と、ケース名、クラス名まで一緒に伝えるのがポイントです。Rorkは指示が具体的であれば、並列構造のコードを正しく出力します。私はこのアプローチを Ukiyo-e Wallpapers の Android 版リニューアルでも使いましたが、当初の生成結果を受け取った後に上記の指示で再生成させたところ、レビューなしで使えるコードが出てきました。
v2.1.0適用後に変わったこと
Beautiful HD Wallpapers の v2.1.0 でこの修正を適用してから、広告関連のバグ報告がゼロになりました。以前は月に1〜2件あった「広告が出ない」「広告が出すぎる」という報告が消え、カスタマーサポートの負荷が下がっています。
コードレビューの観点でも変化があります。ad-gate のロジックを読む際に when の各ブランチを上から順に追えばよくなったため、新機能追加時の確認時間が短くなりました。個人開発では実装とレビューが同一人物なので、「未来の自分がすぐ読める」かどうかがコードの品質基準になります。入れ子を並列に直した後のコードは、その基準を明確に満たしています。
ad-free判定が絡む広告ゲートを実装しているなら、まず grep -r "if.*isAdFree" src/ で入れ子パターンを探してみてください。入れ子になっている箇所が見つかれば、今日から並列構造へ書き直す価値があります。
なぜこのパターンが繰り返されるのか
入れ子のad gateは、最初に書くときには合理的に見えます。「広告フリーなら戻る、そうでなければ広告条件を確認する」という日本語の論理をそのままコードに落とすと、自然と入れ子になります。公式のAdMobサンプルコードも入れ子で書かれていることがあり、それを参考にすると同じ構造になりやすいです。
問題が顕在化するのは、ほとんどの場合「後から条件を追加したとき」です。初期リリースでは課金ユーザーだけad-freeにすれば十分だったのが、報酬広告の免除、A/Bテスト中の広告スキップ、特定画面での広告抑制、開発時のデバッグフラグ——こういった要件が積み重なるにつれて、入れ子の深さが増していきます。
個人開発では、アプリを長期間运営していると必ずこの状況に直面します。Beautiful HD Wallpapers は2013年にリリースして12年以上経っており、ad-gate のロジックには様々な時代の判断が積み重なっていました。並列構造への書き直しは、そのコードの歴史を整理する作業でもありました。
全体を振り返ってと次のアクション
入れ子のad gateを並列構造に書き直すだけで、コードの可読性と保守性が大きく改善されます。Rorkで生成したコードを受け取った後にも、この観点でのレビューを習慣にすることをおすすめします。
既存コードに入れ子のad gateがないか、まず grep -r "if.*isAdFree" src/ または grep -r "isAdFree" app/ で確認してみてください。ヒットした箇所を when ブロックに書き換えることが、今日からできる最初のステップです。
ad-free状態の Source of Truth については、アプリのビジネスロジックの中心に置くほど効果があります。広告表示だけでなく、プレミアム機能の解放可否や特定のUIの表示制御など、「課金状態を参照したい場面」が増えるほど、AdFreeManager のような一元管理クラスの価値が高まります。最初から設計しておくか、後から段階的に移行するかは開発フェーズによりますが、広告収益が中心のアプリでは早い段階で導入しておく方が後の作業を大きく減らせます。Rorkとのやり取りでこの設計を共有しておくことで、AIが生成するコードの品質も自然と上がります。