STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

enableEdgeToEdge のデフォルト引数を理解しよう

こんにちは、naberyo(@error96num)です。今年4月に STORES へ入社し、 STORES ブランドアプリ のAndroidエンジニアをしています。

Androidエンジニアのみなさま、アプリのターゲットSDKは35に上げましたか?
もし上げたなら、エッジツーエッジ対応もお済みでしょうか?

Android 15 (SDK 35) 以降をターゲットとするアプリでは、画面の端から端までコンテンツが描画される「エッジツーエッジ」がデフォルトで適用 されるようになりました。この変更は、没入感あるUXを実現する素晴らしい機能ですが、対応を間違えると思わぬ落とし穴にはまる可能性があります。

先日、弊社のYamatonが『edge-to-edge対応Tips』という包括的な解説記事を公開しました。まだご覧になっていない方は、ぜひ一度チェックしてみてください。

product.st.inc

この記事ではその解説を踏まえつつ、私がエッジツーエッジ対応で直面した「enableEdgeToEdge関数」に関する意外な落とし穴と、その対処法を紹介します。同じ問題で悩むことがないよう、ぜひ参考にしてください!

はじめに

アプリが SDK 35 以降をターゲットとしている場合、Android 15 以降のデバイスでは、エッジツーエッジが自動的に有効になります。これは、アプリ全体のデザインやUXを洗練させる素晴らしい機能です。

一方で、SDK 34 以前のバージョンでは、エッジツーエッジを有効にするためには、明示的な設定が必要です。この際、多くの開発者が頼りにするのが、enableEdgeToEdge という便利な関数です。

落とし穴にハマるまで

Androidの公式ドキュメントには、次のように説明されています。

Activity の onCreate で enableEdgeToEdge を呼び出して、エッジ ツー エッジを手動で有効にします。setContentView の前に呼び出す必要があります。

ふむふむ。なるほど、簡単じゃないか!
私は意気揚揚と、各Activityに次のコードを追加しました。

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

これでSDK 34以前のエッジツーエッジ対応完了! そう思った矢先、ビルドしたアプリを確認すると、なんと次のような状態になっていたのです。

Before After

なんですって。画面最上部、ステータスバーのシステムアイコンが見えなくなっている...!?

正確に言うと、黒いアイコンが黒い背景と同化し、完全に消えたように見えてしまったのです。

状況解説

この状態はライトモードで暗い色のステータスバーを使用している、あるいはダークモードで明るい色のステータスバーを使用しているアプリで、enableEdgeToEdgeデフォルト引数のまま呼び出したときに起こります。

また、ダークモードをサポートしていないアプリで暗い色のステータスバーを使っているケースでも同様です。

公式ドキュメントには、次のように説明されています。

デフォルトでは、enableEdgeToEdge() はシステムバーを透明にします。ただし、ステータスバーに半透明のスクリムが適用される 3 ボタン ナビゲーション モードを除きます。システム アイコンとスクリムの色は、システムのライトモードまたはダークモードに基づいて調整されます。

ここで特に重要なのは、システムアイコンの色が、システムのライト/ダークモードに従って決定される という点です。

enableEdgeToEdgeとデフォルト引数の関係

実際にenableEdgeToEdge関数のコードを追ってみると、statusBarStyleのデフォルト引数として SystemBarStyle.auto が指定 されています。

fun ComponentActivity.enableEdgeToEdge(
    statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
    navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim)
) {
        ...
}

SystemBarStyle.autoの中では、以下のような実装でシステムがダークモードかどうかを判断します。

@JvmStatic
@JvmOverloads
fun auto(
    @ColorInt lightScrim: Int,
    @ColorInt darkScrim: Int,
    detectDarkMode: (Resources) -> Boolean = { resources ->
        (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
            Configuration.UI_MODE_NIGHT_YES
    }
): SystemBarStyle {
    return SystemBarStyle(
        lightScrim = lightScrim,
        darkScrim = darkScrim,
        nightMode = UiModeManager.MODE_NIGHT_AUTO,
        detectDarkMode = detectDarkMode
    )
}

そして、この detectDarkMode を用いて、エッジツーエッジ用のAPI (EdgeToEdgeApiXX) がステータスバーとナビゲーションバーのスタイルを切り替えており、具体的には ライトモード時は黒いアイコン、ダークモード時は白いアイコン を描画する挙動を設定しています。

@JvmName("enable")
@JvmOverloads
fun ComponentActivity.enableEdgeToEdge(
    statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
    navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim)
) {
    val view = window.decorView
    val statusBarIsDark = statusBarStyle.detectDarkMode(view.resources)
    val navigationBarIsDark = navigationBarStyle.detectDarkMode(view.resources)
    val impl =
        Impl
            ?: if (Build.VERSION.SDK_INT >= 30) {
                EdgeToEdgeApi30()
            } else if (Build.VERSION.SDK_INT >= 29) {
                EdgeToEdgeApi29()
            } else if (Build.VERSION.SDK_INT >= 28) {
                EdgeToEdgeApi28()
            } else if (Build.VERSION.SDK_INT >= 26) {
                EdgeToEdgeApi26()
            } else if (Build.VERSION.SDK_INT >= 23) {
                EdgeToEdgeApi23()
            } else
                if (Build.VERSION.SDK_INT >= 21) {
                        EdgeToEdgeApi21()
                    } else {
                        EdgeToEdgeBase()
                    }
                    .also { Impl = it }
    impl.setUp(
        statusBarStyle,
        navigationBarStyle,
        window,
        view,
        statusBarIsDark,
        navigationBarIsDark
    )
    impl.adjustLayoutInDisplayCutoutMode(window)
}

例えば、EdgeToEdgeApi29の実装では、次のようにステータスバーやナビゲーションバーのスタイルを更新しています。

WindowInsetsControllerCompat(window, view).run {
    isAppearanceLightStatusBars = !statusBarIsDark
    isAppearanceLightNavigationBars = !navigationBarIsDark
}

黒いアイコンが背景に同化する原因

こうした仕組みにより、「ライトモード=明るいステータスバーで黒いアイコン」「ダークモード=暗いステータスバーで白いアイコン」 という前提がデフォルトになっています。

  • ライトモードなのにステータスバーを暗い色で設定しているアプリの場合、黒いアイコンが暗い背景に溶け込み、消えたように見えてしまいます。
  • ダークモードなのにステータスバーを明るい色にしている場合も、白いアイコンが明るい背景に溶け込み、消えたように見えてしまいます。

まとめ:enableEdgeToEdgeのデフォルト動作

  • SystemBarStyle.auto はシステムのモード設定(ライト/ダーク)に基づいて、ステータスバーの色とアイコン色を自動的に切り替える。
  • デフォルトでは、ライトモード = 黒アイコン / ダークモード = 白アイコン という前提がある。
  • アプリ固有でステータスバーやナビゲーションバーを独自の色にしている場合、この前提と合わずにアイコンが背景に紛れ込む可能性がある。

対応

これまで紹介したように、enableEdgeToEdge 関数はシステムのライト/ダークモードに応じてステータスバーのスタイルを自動調整します。

しかし、アプリ側でステータスバーの色を独自に設定している場合や、ライト/ダークモードの切り替えをサポートしない場合には、デフォルトの挙動だけでは問題が生じる可能性があります。

そこで、本章では enableEdgeToEdgeのデフォルト引数を理解した上で、正しく使うためのポイント を紹介します。

まずは、デフォルト引数である SystemBarStyle.autoKDocから引用・翻訳してみましょう。

このスタイルは自動的にダーク モードを検出し、ステータス バーとナビゲーション バーのそれぞれに推奨されるスタイルを適用します。このスタイルがアプリで機能しない場合は、darkまたはlightの使用を検討してください。

ここにあるとおり、自動的な判断がアプリの要件と合わない場合には、darklightへの切り替えが推奨されています。次のセクションでは、最小構成の対応例から、より柔軟なアプローチまで順を追って解説していきます。

最小構成の対応例

例えば、システムのライト/ダークモード設定によらず、常に暗い色のステータスバーを表示したい場合には、enableEdgeToEdgeを以下のように呼び出します。

override fun onCreate(savedInstanceState: Bundle?) {
    enableEdgeToEdge(
        statusBarStyle = SystemBarStyle.dark(
            scrim = Color.TRANSPARENT,
        ),
    )
    super.onCreate(savedInstanceState)
    ...
}

より柔軟な対応例

ここからはオプショナルな内容ですが、 STORES ブランドアプリ ではもう少し柔軟にステータスバーの色を制御しています。

「システムのモード設定とは無関係に、アプリのテーマでステータスバーを統一したい」「複数のテーマを切り替えたい」など、アプリ独自の要件がある場合に参考になるアプローチです。

まず、次のような拡張関数を定義します。これは、アプリテーマで設定された windowLightStatusBar の値を参照し、エッジツーエッジを適用する前のステータスバー色を引き継ぐための仕組みです。

fun ComponentActivity.enableEdgeToEdgeForBrandApp() {
    when (windowLightStatusBar) {
        true -> enableEdgeToEdge(
            statusBarStyle = SystemBarStyle.light(
                scrim = Color.TRANSPARENT,
                darkScrim = Color.TRANSPARENT,
            ),
        )
        false -> enableEdgeToEdge(
            statusBarStyle = SystemBarStyle.dark(
                scrim = Color.TRANSPARENT,
            ),
        )
    }
}

ここで、windowLightStatusBar はエッジツーエッジ適用前のステータスバー色を示し、以下のように定義します。

val Context.windowLightStatusBar: Boolean
    get() {
        val typedValue = TypedValue()
        theme.resolveAttribute(android.R.attr.windowLightStatusBar, typedValue, true)
        return typedValue.data != 0 // 0 (false) is dark mode, -1 (true) is light mode
    }

この拡張関数を ActivityonCreate で呼び出すことで、エッジツーエッジを有効にしつつ、アプリ側が指定したテーマ色に合わせてステータスバーのスタイルを適用できるようになります。

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

解決

以上の対応で、無事にステータスバーのアイコンが正しく表示されるようになりました。

おわりに

この記事では、enableEdgeToEdge 関数のデフォルト引数が、アプリの表示にどのような影響を及ぼすのかを解説しました。特に、システムのライト/ダークモード設定との依存関係を理解することで、エッジツーエッジをより正しく・効果的に活用できるようになります。

  • 最小構成の対応例では、システムのモード設定に影響されず「常に暗い色」を適用する方法を紹介しました。
  • より柔軟な対応例では、アプリ独自のテーマ設定に合わせてステータスバーを制御する手法を示しました。

まずは、enableEdgeToEdge のデフォルト挙動をアプリで試し、自分のアプリがどのように表示されるかを確認してみてください。その上で、アプリが採用しているテーマやデザイン方針に合ったスタイルを選択し、エッジツーエッジを最大限に活用して没入感のあるUXを提供していきましょう!

今後もAndroidのUI仕様はアップデートが続くため、公式ドキュメントのチェックや実機でのテストは欠かせません。この記事が、その一助となれば幸いです。