Jetpack Compose における edge-to-edge の正しい実装パターン——インセット処理の責任分担で実装を簡潔に

2026年5月19日火曜日

Android 技術記事 手順チェッカー

t f B! P L

概要

Android 15 以降、edge-to-edge レイアウトが標準化されます。Compose アプリで edge-to-edge を正しく実装するには、enableEdgeToEdge() を呼ぶだけでなく、各 UI コンポーネントが ステータスバー・ナビゲーションバー・IME(入力方式エディター)のウィンドウインセットを適切に処理する必要があります。本記事では、インセット処理の責任分担パターンを提案し、実装例を交えて解説します。

背景・問題

Android 15(targetSdk 35+)では edge-to-edge が強制的に有効化されます。従来は `setDecorFitsSystemWindows(false)` を明示的に呼ぶ必要がありましたが、今後はデフォルト動作になります。

Jetpack Compose でこれに対応しようとすると、複数の問題に直面します。

  • コンテンツの隠れ:インセットを処理しないと、コンテンツがステータスバーやナビゲーションバーに隠れてしまいます。
  • 二重カウント:複数のコンポーネントが同じインセットを処理すると、意図しない余白が生まれます。例えば、Material3 の `Scaffold` がステータスバーのパディングを加え、さらに `TopAppBar` も処理すれば、余分な余白が発生します。
  • IME との干渉:`windowSoftInputMode="adjustResize"` と `enableEdgeToEdge()` の組み合わせは、Android 11 以下で IME パディングの二重適用を招きます。

これらの問題を回避するには、各コンポーネントが責任分担してインセットを処理する設計が有効です。

解決策

1. enableEdgeToEdge() の呼び出し位置

enableEdgeToEdge() は `onCreate()` の冒頭で、super.onCreate() よりも前に呼んでください。これにより、初期フレームから edge-to-edge が有効になります。

override fun onCreate(savedInstanceState: Bundle?) {
    enableEdgeToEdge()        // super より前に呼ぶ
    super.onCreate(savedInstanceState)
    setContent { ... }
}

2. インセット処理の責任分担

edge-to-edge では、コンテンツ領域全体がシステムバー配下に拡張されます。この領域を有効に使うため、以下の責任分担パターンを採用します。

  • ステータスバー:`TopAppBar` に任せる
  • ナビゲーションバー:バナー広告(または固定フッター)に任せる
  • IME:入力フィールドの親コンテナで `imePadding()` を使う

各コンポーネントが 1 つの責務に絞ることで、二重カウントを防ぎ、コードの意図が明確になります。

3. Scaffold の contentWindowInsets をゼロに

Material3 の `Scaffold` はデフォルトで、ステータスバー・ナビゲーションバーのインセットを `paddingValues` に含めます。各コンポーネント(TopAppBar・BannerAd 等)が個別にインセットを処理する場合は、二重カウントを防ぐため、Scaffold の `contentWindowInsets` にゼロを指定します。

Scaffold(
    contentWindowInsets = WindowInsets(0, 0, 0, 0),
    topBar = { TopAppBar(...) },  // windowInsets はデフォルトのまま
) { paddingValues ->
    ...
}

4. TopAppBar のデフォルト動作

`TopAppBar` のデフォルト `windowInsets` は `TopAppBarDefaults.windowInsets`(= `WindowInsets.statusBars`)に設定されています。これにより、TopAppBar の背景色はステータスバー領域まで拡張され、内部コンテンツはステータスバー分だけ下に押し下げられます。特に修正は不要です。

5. ナビゲーションバーはバナー広告で処理する

バナー広告がある場合、ナビゲーションバーの処理をバナーに委ねるパターンが有効です。以下の 3 つの modifier を組み合わせます。

  • height(AdSize.BANNER.height.dp + navigationBarHeight):バナー高さ + ナビゲーションバー高さ
  • background(MaterialTheme.colorScheme.surfaceVariant):ナビゲーションバー領域の背景色を設定
  • navigationBarsPadding():AdView 自体の描画をナビゲーションバーの上に限定

6. NavigationBar の二重カウント回避

Material3 の `NavigationBar` はデフォルトでナビゲーションバーのインセットを内部パディングに追加します。バナー広告が既にナビゲーションバーを処理している場合は、`NavigationBar` に `windowInsets = WindowInsets(0)` を指定して、二重カウントを防ぎます。

7. IME は imePadding() のみで制御

`windowSoftInputMode="adjustResize"` と `enableEdgeToEdge()` の組み合わせは、古い Android バージョンで IME パディングの二重適用を招きます。

AndroidManifest.xml から `windowSoftInputMode="adjustResize"` を削除し、Compose 側では入力フィールドの親コンテナで `imePadding()` のみを使用してください。

Surface(modifier = modifier.fillMaxSize()) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .imePadding()  // IME 表示時に下部パディングを追加
    ) {
        TextField(...)
        LazyColumn(modifier = Modifier.weight(1f)) { ... }
    }
}

LazyColumn に `weight(1f)` を指定することで、IME 表示時に Column の縮小分が LazyColumn に集中し、フォーカス中のテキストフィールドがキーボードに隠れません。

まとめ

Jetpack Compose で edge-to-edge を正しく実装するには、以下の 4 つのポイントが重要です。

  • enableEdgeToEdge() を `super.onCreate()` より前に呼ぶ
  • Scaffold の `contentWindowInsets` にゼロを指定し、各コンポーネントが個別にインセットを処理する
  • ナビゲーションバーの処理をバナー広告(または固定フッター)に委ねることで、NavigationBar の `windowInsets` をゼロに設定できる
  • IME は `imePadding()` のみで制御し、`windowSoftInputMode="adjustResize"` を削除する

これらのパターンを採用することで、インセット処理の意図が明確になり、コードが簡潔で保守しやすくなります。手順チェッカーアプリで実装された例を参考にしながら、自身のアプリに適用してみてください。

このブログを検索

Smart Hide Launcher の名称を Hitoma Launcher に変更しました

Smart Hide Launcher の名称を、Hitoma Launcher に変更しました。アプリアイコンも新しくなります。 コンセプトは「必要なアプリだけ」です。 これまでの名前は、アプリを「隠す」という機能を中心に表していました。今回の名称変更では、必要なアプリを選...

QooQ