🐞

accompanist.systemuicontroller を enableEdgeToEdge に置き換える

に公開

ポート株式会社 サービス開発部 Advent Calendar 2024 の1日目です。

初めに

ポート株式会社でAndroid開発をしている@shxun6934 です。

弊社のAndroidアプリでは、一部の画面でaccompanistsystemuicontrollerを使用していました。
ですが、このライブラリーはv0.34.0からDeprecatedになっています。

今回はその移行に関して行ったことや、知見をまとめておこうと思います!

accompanist.systemuicontrollerとは

accompanist.systemuicontrollerは、ComposeでシステムUIバー(ステータスバー・ナビゲーションバー)の色を更新するためのライブラリーです。

例えば、ステータスバーの色を黒色にしたい場合は、以下のようなコードで実現できます。

// システムUIの状態を取得
val systemUiController = rememberSystemUiController()

// システムUIの状態が変わる or Composeが退場するときに変更
DisposableEffect(systemUiController) {
    // ステータスバーを黒色に変える
    systemUiController.setStatusBarColor(color = Color.Black)

    onDispose {}
}
default black

置き換え先の検討・方針

置き換え先を検討するにあたり、まずは公式を確認しました。

公式から、androidx.activity 1.8.0-alpha03以降から利用できる ComponentActivity.enableEdgeToEdge を使用することが推奨されていました。

また、nowInAndroidaccompanist.systemuicontrollerからenableEdgeToEdgeへの移行を行っていたため、この実装を参考に、弊社のAndroidアプリもaccompanist.systemuicontrollerからenableEdgeToEdgeを行うことにしました。

enableEdgeToEdge

上記通り、androidx.activity 1.8.0-alpha03から使用できるComponentActivityの拡張関数です。
アプリのUIをデバイスの端から端まで表示するために使用します。

すなわち、アプリのUIがシステムUIバーの裏側に表示されるようにするということです。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       
        // EdgeToEdge を適用
        // setContent よりも前に呼び出す必要がある
        enableEdgeToEdge()

        setContent {
            /* 中略 */
        }
    }
}

ComposeではScaffoldpaddingScaffold内で定義しているComposeに適用しない限り、システムUIバーに被る形で描画されるようになります。

適用前 適用後
Light (ジェスチャー)
Dark (ジェスチャー)
Light (3ボタン)
Dark (3ボタン)

また、enableEdgeToEdgestatusBarStylenavigationBarStyleを渡すことで、ステータスバー・ナビゲーションバーのスタイルをカスタマイズすることができます。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       
        // EdgeToEdge を適用
        enableEdgeToEdge(
            // ステータスバーをライトモード・ダークモードに限らず、常に黒色に設定
            statusBarStyle = SystemBarStyle.dark(
                scrim = Color.Black.toArgb()
            ),
            // ナビゲーションバーをライトモード・ダークモード、ナビゲーションモードに限らず、常に白色に設定
            navigationBarStyle = SystemBarStyle.light(
                scrim = Color.White.toArgb(),
                darkScrim = Color.White.toArgb()
            )
        )

        setContent {
            /* 中略 */
        }
    }
}
Light Dark
ジェスチャー
3ボタン

SystemBarStyle

システムUIバーをカスタマイズするために使用するクラスです。

SystemBarStyleから公開されている3つのメソッドのいずれかを使用して、カスタマイズしていきます。

  • SystemBarStyle.auto
    • デバイスがダークモードかどうかを自動で検出し、ステータスバー・ナビゲーションバーに推奨スタイルを適用する
    • APIレベル 29 以上は、ステータスバー・ナビゲーションバーの両方が透明になる
    • APIレベル 28 以下は、ステータスバーは透明になり、ナビゲーションバーにはダークモードに応じて指定されたスクリムカラーが表示される
  • SystemBarStyle.dark
    • ナビゲーションモードに関係なく、ダークモードのスクリムカラーを適用する
  • System.light
    • ナビゲーションモードに関係なく、ライトモードのスクリムカラーを適用する

* スクリムとは、ステータスバーやナビゲーションバーの上に重ねる半透明なコンポーネントのこと。
重ねられている要素に焦点を当てたい場合に使用します。
https://m3.material.io/styles/elevation/applying-elevation#92b9fb39-f0c4-4829-8e4d-97ac512976aa

実際の移行作業

弊社のAndroidアプリでのaccompanist.systemuicontrollerの使い方は、WindowCompat.setDecorFitsSystemWindows でUIをステータスバーの裏側まで表示し、ステータスバーの色を透明にしていました。

この内容は、enableEdgeToEdge内の実装コードに内包されているため、enableEdgeToEdgeを適用するだけで問題なかったです。

ですが、enableEdgeToEdgeAndroidのAPIレベルによって実行されるコードが変わります

APIレベル 29 以上の場合はEdgeToEdgeApi29を、APIレベル 26 以上の場合はEdgeToEdgeApi26を実行するという形になっています。

fun ComponentActivity.enableEdgeToEdge(
    statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
    navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim)
) {
    // 実行しているAPIレベルによって、実行するメソッドを変更する
    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)
}
@RequiresApi(29)
private open class EdgeToEdgeApi29 : EdgeToEdgeApi28() {

    override fun setUp(
        statusBarStyle: SystemBarStyle,
        navigationBarStyle: SystemBarStyle,
        window: Window,
        view: View,
        statusBarIsDark: Boolean,
        navigationBarIsDark: Boolean
    ) {
        // アプリをシステムUIバーの裏にも描画するように設定する
        WindowCompat.setDecorFitsSystemWindows(window, false)
       
        // ステータスバー・ナビゲーションバーの色を設定する
        // SystemBarStyle.auto にしている場合は透明色になる
        // それ以外の場合はライトモード・ダークモードによって変わる
        window.statusBarColor = statusBarStyle.getScrimWithEnforcedContrast(statusBarIsDark)
        window.navigationBarColor =
            navigationBarStyle.getScrimWithEnforcedContrast(navigationBarIsDark)

        // 完全に透明な背景色(= アルファ値が0)になった場合、システムがステータスバー・ナビゲーションバーに十分なコントラストを確保するかどうかを設定する
        // ステータスバーは常にしない
        // ナビゲーションバーはSystemBarStyleの設定によって変わる
        window.isStatusBarContrastEnforced = false
        window.isNavigationBarContrastEnforced =
            navigationBarStyle.nightMode == UiModeManager.MODE_NIGHT_AUTO

        // ステータスバー・ナビゲーションバーのフォアグラウンドをライトモード用に設定するかどうかを設定する
        // == アイコン・フォントカラーの変更
        WindowInsetsControllerCompat(window, view).run {
            isAppearanceLightStatusBars = !statusBarIsDark
            isAppearanceLightNavigationBars = !navigationBarIsDark
        }
    }
}
private open class EdgeToEdgeApi26 : EdgeToEdgeBase() {

    override fun setUp(
        statusBarStyle: SystemBarStyle,
        navigationBarStyle: SystemBarStyle,
        window: Window,
        view: View,
        statusBarIsDark: Boolean,
        navigationBarIsDark: Boolean
    ) {
        // アプリをシステムUIバーの裏にも描画するように設定する
        WindowCompat.setDecorFitsSystemWindows(window, false)

        // ステータスバー・ナビゲーションバーの色を設定する
        // ライトモード・ダークモードによって変わる
        window.statusBarColor = statusBarStyle.getScrim(statusBarIsDark)
        window.navigationBarColor = navigationBarStyle.getScrim(navigationBarIsDark)

        // ステータスバー・ナビゲーションバーのフォアグラウンドをライトモード用に設定するかどうかを設定する
        // == アイコン・フォントカラーの変更
        WindowInsetsControllerCompat(window, view).run {
            isAppearanceLightStatusBars = !statusBarIsDark
            isAppearanceLightNavigationBars = !navigationBarIsDark
        }
    }
}

そのため、APIレベル、ライトモード・ダークモード、ナビゲーションモードによってデザインが変わらないように少し調整する必要がありました。

(実際の対応を行ったPRの動作確認説明)

また、今回の移行では、androidx.compose.navigationを使用して、表示する画面を変えていたため、表示する画面によってシステムUIバーのデザインを変更する必要がありました。

そのため、副作用とWindowInsetsを使用して、enableEdgeToEdgeの実装内容を上書きしました。

val view = LocalView.current
val context = LocalContext.current

// Composition 毎に変更する
SideEffect {
    context.findActivity()?.let { activity ->
        WindowCompat.getInsetsController(activity.window, view).run {
            // ライトモードの場合、ステータスバーが見づらくなってしまうため、ステータスバーの文字色をダークモード固定にする
            isAppearanceLightStatusBars = false

            // ナビゲーションバーのスタイルをダークモード固定にする
            isAppearanceLightNavigationBars = false
        }
    }
}

fun Context.findActivity(): Activity? =
    when (this) {
        is Activity -> this
        is ContextWrapper -> this.baseContext.findActivity()
        else -> null
    }

これらの内容で、弊社は無事にaccompanist.systemuicontrollerからenableEdgeToEdgeへ移行することができました!

移行作業を終えての感想

対象画面が一部だったとはいえ、APIレベルによって、ダークモードがなかったり、3ボタンしかなかったり認め、どの条件でもデザインが崩れていないか、前バージョンと差異があまり発生しないかを確認するのがとても大変でした。

また、デザインが変わってしまう部分がどうしてもあり、デザイナーさんにも協力していただきながら対応を進めることになったため、思っていたよりも大掛かりな対応になって、連携の部分でも大変な思いをしました。

ですが、Android 15 をターゲットにするとEdgeToEdgeが自動で適用されるため、その前準備がデザイナーさんとエンジニアでできたことは良かったなと思っています。

終わりに

今回は、accompanist.systemuicontrollerenableEdgeToEdgeに置き換える内容を記事にしました。

特にenableEdgeToEdgeに関して少しでも役に立つ記事になれたら幸いです。

2日目も僕なので、明日もみてください!

参考

ポート株式会社 エンジニアブログ

Discussion