Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

setContentView前はinsetsControllerがnullでImmersiveが効かない

公開 読了時間 約5分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

window.insetsController は DecorView が確定する setContentView() の呼び出しより前では null を返すため、その時点で hide() を呼んでも Immersive モードは適用されない。

結論

window.insetsControllersetContentView() を呼ぶ前では null を返します。 DecorView が確定していないためで、null セーフに書いていればクラッシュもしません。その代わり、Immersiveモードの hide() が一度も実行されないまま、何も起きなかったかのように処理が終わります。

症状

サイネージ端末で常時フルスクリーン表示させたい ActivityonCreate() に、こう書いていました。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    window.addFlags(
        WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
            WindowManager.LayoutParams.FLAG_FULLSCREEN or
            WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
    )

    // フルスクリーン Immersive
    enterImmersiveMode()

    val root = FrameLayout(this).apply { setBackgroundColor(Color.BLACK) }
    // ... WebView などを root に addView ...

    setContentView(root)
    // ...
}

enterImmersiveMode()setContentView(root) より前に呼んでいます。ビルドは通り、実行してもクラッシュしません。ですが実機で見ると、ステータスバーやナビゲーションバーが消えません

原因

enterImmersiveMode() の中身はこうなっていました(Android 11 / API 30 以降の分岐)。

private fun enterImmersiveMode() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        window.setDecorFitsSystemWindows(false)
        window.insetsController?.let {
            it.hide(android.view.WindowInsets.Type.systemBars())
            it.systemBarsBehavior =
                android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
        }
    } else {
        // ...
    }
}

window.insetsController は、DecorView が確定してからでないと取得できません。DecorView は setContentView() を呼んだタイミングで確定するため、それより前に insetsController へアクセスすると null が返ります。

問題は、この呼び出しが ?.let { ... } という null セーフな形で書かれていたことです。null のときは it.hide(...) を含むブロックごと静かにスキップされ、例外もログも出ません。「呼んだのに効いていない」状態が、クラッシュという分かりやすい形で現れないため、画面を目視するまで気づけませんでした。

直し方

enterImmersiveMode() の呼び出し位置を setContentView(root) の後ろへ移しました。

setContentView(root)

// Immersive モードは setContentView 後に呼ぶ(insetsController は DecorView 確定後に有効)
enterImmersiveMode()

これだけでも直りますが、insetsController 周りの呼び出しはそれぞれ try-catch で囲み、null や例外が起きても他の初期化処理を止めない形に変更しました。

private fun enterImmersiveMode() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        try {
            window.setDecorFitsSystemWindows(false)
        } catch (_: Throwable) {}
        window.insetsController?.let {
            try {
                it.hide(android.view.WindowInsets.Type.systemBars())
                it.systemBarsBehavior =
                    android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
            } catch (_: Throwable) {}
        }
    } else {
        // ...
    }
}

再発防止

呼び出し順序を直すだけでは、また別の初期化処理を setContentView() より前に足したときに同じ落とし穴を踏みます。今回は setDecorFitsSystemWindowsinsetsController への呼び出しをそれぞれ try-catch で囲み、たとえ順序を間違えて null や例外が起きても、Immersive化の失敗だけに留めて他の初期化を止めないようにしました。null セーフな ?.let は「クラッシュしない」ことと「意図通り動く」ことを混同させるので、DecorView に依存する API を呼ぶときは、確定前後のどちらでも安全に倒れる書き方をセットで入れる必要があります。

よくある質問

Q1setContentView前にinsetsControllerを呼ぶと何が起きますか

window.insetsController が null を返します。呼び出しを ?.let { ... } の形で null セーフに書いていると例外も出ないため、hide() が一度も実行されないままImmersive化の処理だけが静かにスキップされます。

Q2エラーが出ないのに気づきにくいのはなぜですか

insetsController の呼び出しを ?.let で包んでいると、null のときは中の処理ごと無視されて何のログも出ません。クラッシュしないぶん、画面のステータスバーが消えていないことを目視で確認するまで気づけません。

Q3Android 10以前でも同じ問題が起きますか

起きません。この問題は Build.VERSION_CODES.R(API 30)以降で使う window.insetsController 経路だけの話です。R未満は systemUiVisibility を直接書き換える別経路なので、DecorView の確定タイミングに依存しません。

Q4呼び出し順序を直すだけで再発しませんか

順序を直しただけでは、将来また誰かが同じ落とし穴を踏む可能性が残ります。setDecorFitsSystemWindows と insetsController への呼び出しをそれぞれ try-catch で囲み、null や例外が起きても他の初期化処理を止めない形にしました。

確認した環境

  • compileSdk 34 / targetSdk 34 / minSdk 26(Kotlin, AppCompatActivity)
  • 2026-05-30 に発生・同日修正

この記事の根拠

  • Kotlinファイル 78〜119行目コミット 9f413fa
  • Kotlinファイル 192〜199行目コミット 9f413fa
  • Kotlinファイル 78〜119行目コミット f11de55
  • Kotlinファイル 192〜203行目コミット f11de55

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。