CLAUDE LABEN
FABLE5 — Claude Fable 5が公開(6/9)。Opusを上回るMythosクラス初の一般提供モデルで、100万トークンコンテキスト・128k出力・常時適応思考を搭載FREE-WINDOW — Fable 5は6/22までPro/Max/Team/Enterpriseに無料同梱。6/23以降はusage creditsが必要。APIは入力$10/出力$50 per MTokSAFEGUARDS — Fable 5は高リスク領域の質問をOpus 4.8へ自動フォールバック(発動はセッションの5%未満)。制限解除版のMythos 5は審査済み組織限定IPO — AnthropicがIPOを機密申請(6/1)。直近調達$65B・評価額$965B・年換算売上$47Bと報道BILLING — 6/15の課金変更まで残り3日。Agent SDK・headless Claude Code・GitHub Actions・他社エージェントがAPIレート準拠の月次クレジットへ移行しますPLATFORM — Claude Developer PlatformにManaged Agentsのスケジュールデプロイ・vault環境変数クレデンシャル・セッションスレッドWebhookが追加FABLE5 — Claude Fable 5が公開(6/9)。Opusを上回るMythosクラス初の一般提供モデルで、100万トークンコンテキスト・128k出力・常時適応思考を搭載FREE-WINDOW — Fable 5は6/22までPro/Max/Team/Enterpriseに無料同梱。6/23以降はusage creditsが必要。APIは入力$10/出力$50 per MTokSAFEGUARDS — Fable 5は高リスク領域の質問をOpus 4.8へ自動フォールバック(発動はセッションの5%未満)。制限解除版のMythos 5は審査済み組織限定IPO — AnthropicがIPOを機密申請(6/1)。直近調達$65B・評価額$965B・年換算売上$47Bと報道BILLING — 6/15の課金変更まで残り3日。Agent SDK・headless Claude Code・GitHub Actions・他社エージェントがAPIレート準拠の月次クレジットへ移行しますPLATFORM — Claude Developer PlatformにManaged Agentsのスケジュールデプロイ・vault環境変数クレデンシャル・セッションスレッドWebhookが追加
記事一覧/Claude Code
Claude Code/2026-06-12中級

Android でペイウォールとレビュー誘導が重なって表示される競合を ModalGate で解決した実装メモ

ペイウォール・レビュー誘導・リワード広告案内の3つのダイアログが同時に表示される競合を、優先度付きの中央ゲート(ModalGate)で解決した実装メモです。Claude Code とのレビューで見つかった閉じ漏れの3経路と Kotlin 実装例を紹介します。

claude-code115android9kotlin3dialogapp-dev

2026年5月、運営している壁紙アプリの Android 版を v2.1.0 へ更新する作業の終盤で、テスト端末のスクリーンショットに目を疑いました。ペイウォールの上にレビュー誘導ダイアログが重なり、その背後ではリワード広告の案内まで開こうとしていたのです。

個人開発を2014年から続けてきて、モーダルの出し分けは長らく「動いているからよし」で済ませてきた部分でした。表示物がペイウォール1つだけの時代は、それで何も起きません。ところが今回の更新でペイウォール(PaywallDialog)、レビュー誘導(ReviewInduction)、リワード広告案内(RewardedIntersDialog)と3つに増えた途端、この暗黙の前提が崩れました。

最終的には ModalGate と名付けた小さな中央管理クラスを1つ導入して解決したのですが、その過程で Claude Code に設計レビューを依頼したことが想像以上に効きました。「ダイアログが重なる」「一度閉じたら以後何も表示されなくなる」といった症状に悩んでいる方に向けて、見つかった罠と実装をまとめます。

モーダルが重なる3つの罠

調べていくと、症状は1つではなく3つの別々の罠が絡み合っていました。

罠1: 各ダイアログが互いの存在を知らない

ペイウォールは「未課金かつ起動3回目以降」、レビュー誘導は「インストール7日経過かつ未レビュー」、リワード案内は「広告閲覧権が未消化」と、それぞれが自分の表示条件だけを見て自己判断していました。条件が独立している以上、すべてが同時に真になる日が必ず来ます。私の場合はインストール7日後に課金せず使い続けたユーザーがその交差点でした。

罠2: 優先度が呼び出し順に暗黙依存する

3つの表示判定は MainActivity の onResume に並んでいたため、「先に書いてある方が先に出る」という呼び出し順だけが事実上の優先度でした。後日、初期化処理の整理でこの順番を入れ替えたところ、出るダイアログが変わってしまいました。コードのどこにも「ペイウォールが最優先」とは書かれていないので、レビューでも気づけません。

罠3: dismiss 連鎖とフラグ解放漏れ

一時しのぎとして「表示中フラグ」を bool で持たせた時期もありました。ところがダイアログを閉じる経路は OK ボタンだけではありません。戻るキーや画面外タップで閉じた場合にフラグが下りない経路があり、その状態になると以後どのモーダルも一切表示されなくなります。ペイウォールが出ないのは収益に直結するので、これは重なるよりも深刻でした。

根本原因は「自己判断するダイアログ」

3つの罠に共通する根は、表示の可否を各ダイアログが自分で判断していることです。登場人物が増えるたびに「他の全員の状態を確認するコード」が各所に増えていき、確認漏れが競合になります。

そこで方針を反転させました。各ダイアログは「出たい」と申請するだけにして、出てよいかどうかの判断は1箇所に集めます。これが ModalGate です。

ModalGate の設計 — 優先度付き中央ゲート

設計方針は3つだけです。

  • 表示可否の判断は ModalGate だけが行う(各ダイアログは申請するだけ)
  • 優先度を数値で明示する(ペイウォール > リワード案内 > レビュー誘導)
  • 却下されたモーダルは持ち越さない(閉じた直後に別のモーダルを連鎖表示しない)

以下が実装のほぼ全体です。何かを解決するコードというより、判断を1箇所に集約するための器に近い小ささです。

// ModalGate.kt — モーダル表示の中央管理(シングルトン)
object ModalGate {
 
    // 表示中モーダルのタグ。null なら何も表示されていない
    private var activeTag: String? = null
 
    // 優先度の定義(数値が小さいほど優先)
    enum class Priority(val rank: Int) {
        PAYWALL(0),          // ペイウォール
        REWARDED_PROMO(1),   // リワード広告案内
        REVIEW_INDUCTION(2)  // レビュー誘導
    }
 
    /**
     * 表示要求。許可された場合のみ true を返す。
     * 呼び出し側は true のときだけ実際に show() する。
     */
    @Synchronized
    fun request(tag: String, priority: Priority): Boolean {
        val current = activeTag
        if (current != null) {
            Log.d("ModalGate", "denied: $tag (active=$current)")
            return false // 表示中なら無条件で却下。連鎖表示もしない
        }
        activeTag = tag
        Log.d("ModalGate", "granted: $tag (priority=${priority.rank})")
        return true
    }
 
    /** DialogFragment.onDismiss() から必ず呼ぶ */
    @Synchronized
    fun release(tag: String) {
        if (activeTag == tag) {
            activeTag = null
            Log.d("ModalGate", "released: $tag")
        }
    }
 
    /** プロセス再生成後の復元用(後述の罠対策) */
    @Synchronized
    fun restore(tag: String?) {
        activeTag = tag
    }
}

呼び出し側はこうなります。

// レビュー誘導の表示判定(呼び出し側)
if (shouldShowReviewInduction() &&
    ModalGate.request("review", ModalGate.Priority.REVIEW_INDUCTION)) {
    ReviewInductionDialog().show(supportFragmentManager, "review")
}

ペイウォールとレビュー誘導の条件が同時に真になった場合、ログには次のように出ます。

D/ModalGate: granted: paywall (priority=0)
D/ModalGate: denied: review (active=paywall)
D/ModalGate: released: paywall

1点だけ、迷った末の判断を書いておきます。「却下されたレビュー誘導を、ペイウォールが閉じた直後に出す」連鎖方式も検討しましたが、採用しませんでした。ユーザーから見ると「閉じても閉じても何か出てくるアプリ」になるからです。却下されたモーダルは次回起動時に改めて条件判定されるだけ、と割り切った方が体験は静かになります。レビュー誘導が1日遅れても失うものはほとんどありません。

Claude Code とのレビューで見つかった「閉じ漏れ」の3経路

release() を onDismiss で呼ぶところまでは自力で書けたのですが、罠3の経験があったので、Claude Code に「release が呼ばれない経路をすべて列挙してください」と設計レビューを依頼しました。返ってきた指摘は3つです。

  • 戻るキー・画面外タップ: setOnDismissListener だけに依存すると onCancel 経由のパスで漏れる場合があります。DialogFragment の onDismiss() オーバーライドに一本化することで、どの閉じ方でも必ず通る経路になりました
  • プロセス再生成: 開発者オプションの「アクティビティを保持しない」を有効にした端末では、復帰時に FragmentManager がダイアログを復元する一方、シングルトンの activeTag は初期化されて null に戻ります。ダイアログは出ているのにゲートは空いている、という不整合です。onCreate で FragmentManager のタグを走査して restore() する処理を足しました
  • 画面回転: 再生成のタイミングで表示判定が再実行されると、復元されたダイアログと新規のダイアログが二重に出ます。判定を onResume から「ユーザー操作起点 + 初回起動フロー」に移して回避しました

正直に言うと、2つ目のプロセス再生成は自分だけでは出てこなかった観点です。シングルトンの寿命と FragmentManager の復元タイミングのずれは、実機で「アクティビティを保持しない」を有効にして初めて再現できました。レビューを依頼するときは「正しいか見てください」ではなく「呼ばれない経路を列挙してください」のように、反例を探す形で質問すると精度が上がると感じています。

導入して変わったこと

v2.1.0 のリリースから約1ヶ月、モーダルの同時表示はテスト・本番レポートともに見ていません。罠3で起きていた「以後何も表示されない」状態も、フラグ管理が release() の1経路に集約されたことで根が消えました。リワード広告案内が意図したタイミングで確実に出るようになったのは、AdMob の収益面でも地味に効いています。

副次的な効果として、モーダルを増やすときのレビューが楽になりました。新しいダイアログを足す場合も、Priority に1行足して request() を通すだけで、既存のモーダルとの競合を個別に考える必要がありません。

なお、この v2.1.0 ではモーダル整理と並行してクラッシュ対応も進めていました。経緯は Claude Code でAndroid の RecyclerView クラッシュを28日間で根治した記録 に、リリース後の監視体制は Play Store 段階公開を Claude Code で自動監視する — v2.1.0 クラッシュ対応から学んだリリース戦略 にまとめています。

まとめ — 最初の一歩

ModalGate の実装自体は50行に満たない小さなクラスで、効果の大半は「表示可否の判断を1箇所に集める」という方針決定から来ています。まずは手元のプロジェクトで .show( を grep して、モーダルを出している箇所を列挙してみてください。それぞれが他のモーダルの表示状態を確認していないなら、表示物が3つに増える前にこのパターンを入れておく価値があります。

同じ課題に取り組んでいる方の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Claude Code2026-06-12
Android の戻るボタン広告ゲートを入れ子から並列独立構造へ — Claude Code と進めた終了フロー再設計の記録
戻るボタンの広告が「出ない」と「出すぎる」を同時に起こした原因は、入れ子分岐に埋め込まれた暗黙の優先度でした。壁紙アプリ v2.1.0 で広告ゲートを並列独立構造へ作り直した過程を、Kotlin実装・Claude Codeへの依頼内容・テストまで含めて記録します。
Claude Code2026-05-16
Claude Code でAndroid の RecyclerView クラッシュを28日間で根治した記録
Beautiful HD Wallpapersのv2.0.0リリース後、RecyclerViewのIndexOutOfBoundsExceptionが28日で50ユーザー以上に発生。Claude Codeとの対話で防御的コピーによる根治法を発見した実録。
Claude Code2026-05-27
Claude Code で Glide 5.0.5 の Java 8 API クラッシュを1行で消した話
Beautiful HD Wallpapers v2.0.0 で Android 6.0.1 ユーザーが全員 3 秒以内にクラッシュ。Claude Code との対話で coreLibraryDesugaringEnabled の 1 行だけが足りていなかったことに辿り着いた実体験です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →