Rebounder Tech Blog

Written by the people who actually run these systems in production.

insetsController Is null Before setContentView

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

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

結論

window.insetsController returns null before setContentView() because the DecorView is not yet resolved, so calling hide() at that point never applies immersive mode.

Conclusion

window.insetsController returns null before setContentView() is called. The DecorView is not resolved yet. Written null-safely, it does not crash either — instead the immersive hide() never runs and the code finishes as if nothing had happened.

Symptom

A signage device needed one Activity permanently in fullscreen. Its onCreate() looked like this.

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
    )

    // Fullscreen immersive
    enterImmersiveMode()

    val root = FrameLayout(this).apply { setBackgroundColor(Color.BLACK) }
    // ... addView a WebView and others to root ...

    setContentView(root)
    // ...
}

enterImmersiveMode() is called before setContentView(root). It builds, and it does not crash at runtime. But on the device, the status bar and navigation bar do not go away.

Cause

Here is what enterImmersiveMode() did on the Android 11 / API 30 and later branch.

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 is only available once the DecorView is resolved. The DecorView is resolved when setContentView() runs, so accessing insetsController before that returns null.

The trap is that the call is written null-safely as ?.let { ... }. When it is null, the whole block — including it.hide(...) — is skipped without an exception and without a log line. “Called but had no effect” never shows up as a crash, so nothing surfaces until someone looks at the screen.

The fix

Move the enterImmersiveMode() call after setContentView(root).

setContentView(root)

// Immersive mode must come after setContentView: insetsController is only
// valid once the DecorView is resolved.
enterImmersiveMode()

That alone fixes it, but the insetsController calls were also wrapped in try-catch so that a null or an exception does not stop the rest of the initialisation.

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 {
        // ...
    }
}

Preventing a repeat

Fixing the order alone means the same trap waits for whoever next adds initialisation before setContentView(). Both setDecorFitsSystemWindows and the insetsController calls are now wrapped in try-catch, so that even with the order wrong, only immersive mode fails and the rest of startup continues.

A null-safe ?.let blurs the difference between “does not crash” and “works as intended”. When calling an API that depends on the DecorView, the safe-either-way form has to go in at the same time.

よくある質問

Q1What happens if I call insetsController before setContentView?

window.insetsController returns null. If the call is written null-safely as ?.let { ... }, no exception is thrown either, so hide() simply never runs and the whole immersive setup is skipped in silence.

Q2Why is this so hard to notice when nothing errors?

Wrapping the insetsController call in ?.let means the entire block is skipped when it is null, with no log output. Because it does not crash, you only find out by looking at the screen and seeing the status bar still there.

Q3Does this also affect Android 10 and earlier?

No. This is specific to the window.insetsController path used from Build.VERSION_CODES.R (API 30) onwards. Below R the code writes systemUiVisibility directly, which does not depend on when the DecorView is resolved.

Q4Is fixing the call order enough to stop it recurring?

Order alone leaves the same trap for the next person. Both setDecorFitsSystemWindows and the insetsController calls were also wrapped in try-catch so that a null or an exception fails only immersive mode, without stopping the rest of the initialisation.

確認した環境

  • compileSdk 34 / targetSdk 34 / minSdk 26 (Kotlin, AppCompatActivity)
  • Occurred and fixed on 2026-05-30

この記事の根拠

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

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