🎨

Jetpack Compose でアプリのテーマ切り替えを実装してみた

2022/06/09に公開

はじめに

こちらが完成形です。

  • テーマ切り替えの実装
  • 端末のダークモードON/OFFも検知

プロジェクトを作成する

Jetpack Compose で作っていくので、
新規プロジェクトから Empty Compose Activity を作成します。

[パッケージ名].ui.theme パッケージの下にColor.ktTheme.ktがあることを確認します。

Color.kt の編集

Color.kt を以下のように書き換え、カラーを定義します。
※ 色を決めるのが楽し過ぎて、結果的にここが一番時間かかる

package com.example.sampleapp.ui.theme

import androidx.compose.ui.graphics.Color

object LoidColors {
    val basic = Color(0xFF817244)
    val highlight = Color(0xFFE9CC78)
    val dark = Color(0xFF323A1E)
}
object YorColors {
    val basic = Color(0xFF440000)
    val highlight = Color(0xFFD63C3C)
    val dark = Color(0xFF200000)
}
object AnyaColors {
    val basic = Color(0xFFDD9389)
    val highlight = Color(0xFFC5EE8E)
    val dark = Color(0xFF331713)
}

Theme.kt の編集

カラーパレットを作る

ColorPalette という enum を作ります。

enum class ColorPalette(
    val basic: Color,
    val highlight: Color,
    val dark: Color
) {}

テーマカラーを設定する

ColorPalette に、ロイドさん/ヨルさん/アーニャ の種別を追加します。

enum class ColorPalette(
    val basic: Color,
    val highlight: Color,
    val dark: Color
) {
    LOID(
        LoidColors.basic,
        LoidColors.highlight,
        LoidColors.dark
    ),
    YOR(
        YorColors.basic,
        YorColors.highlight,
        YorColors.dark
    ),
    ANYA(
        AnyaColors.basic,
        AnyaColors.highlight,
        AnyaColors.dark
    );
}

名前とアイコンが取得できるようにする

drawable に使いたい画像を追加しています。

isSystemInDarkTheme() で端末のダークモードON/OFFがわかります。

enum class ColorPalette(
    val basic: Color,
    val highlight: Color,
    val dark: Color
) {
    // これを追加する
    companion object {
        @Composable
        fun ColorPalette.toName(): String {
            return when (this) {
                LOID -> "Loid Forger(Twilight)"
                YOR -> "Yor Forger(Briar Rose)"
                ANYA -> "Anya Forger(Subject 007)"
            }
        }

        @Composable
        fun ColorPalette.toPainter(): Painter {
            return if (isSystemInDarkTheme()) {
                when (this) {
                    LOID -> painterResource(id = R.drawable.loiddark)
                    YOR -> painterResource(id = R.drawable.yordark)
                    ANYA -> painterResource(id = R.drawable.anyadark)
                }
            } else {
                when (this) {
                    LOID -> painterResource(id = R.drawable.loidlight)
                    YOR -> painterResource(id = R.drawable.yorlight)
                    ANYA -> painterResource(id = R.drawable.anyalight)
                }
            }
        }
    }
}

テーマの切り替えを受けてアプリ全体で参照可能にする

ColorPaletteCompositionLocal を作成します。

CompositionLocal がわからない方は調べてみてください。
Composable 同士で値や状態を反映させる時に、引数バケツリレー状態になりがちですが、
テーマカラーのように、ある Composable の配下全てで同じ値を参照したい時に使われます。

internal val LocalColorPalette = staticCompositionLocalOf { ColorPalette.LOID }

他の Composable から、SampleAppTheme.current という形式で参照できるようにします。

object SampleAppTheme {
    val current: ColorPalette
        @Composable
        @ReadOnlyComposable
        get() = LocalColorPalette.current
}

SampleAppTheme は カラーパレットとダークテーマの切り替えを契機に再コンポーズするようにします。

まずステータスバーやナビゲーションバーの色が変わるようにして、
CompositionLocalProvider() に最新のカラーパレットを渡して反映します。

@Composable
fun SampleAppTheme(
    colorPalette: ColorPalette,
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {

    val sysUiController = rememberSystemUiController()
    LaunchedEffect(colorPalette, darkTheme) {
        val color = if (darkTheme) {
            colorPalette.dark
        } else {
            colorPalette.highlight
        }
        sysUiController.setSystemBarsColor(color)
    }

    CompositionLocalProvider(LocalColorPalette provides colorPalette) {
        MaterialTheme(
            colors = lightColors(),
            typography = Typography,
            shapes = Shapes,
            content = content
        )
    }
}

※ accompanist-systemuicontroller を入れています。

UIの実装

テーマをセット&参照

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
	    var themeColor by remember {
                mutableStateOf(ColorPalette.LOID)
            }
            SampleAppTheme(themeColor) {
                Surface(modifier = Modifier.fillMaxSize(), color = SampleAppTheme.current.basic) {
		}
	    }
	}
    }
}

現在選択されているテーマの値は、SampleAppTheme.current.basic といった形で取得できます。

var themeColor に別のテーマを入れたら再コンポーズされてテーマが反映されます。
ボタンが押された時にでも themeColor = ColorPalette.HOGE としてやりましょう。

アイコンの切り替え

先ほど作成したtoPainter() でテーマの画像を取得できます。
current が変われば勝手に切り替わります。

Image(painter = SampleAppTheme.current.toPainter(), contentDescription = "")

テキストの切り替え

これも同じ感じ。

Text(
    text = SampleAppTheme.current.toName(),
    color = SampleAppTheme.current.highlight
)

できた

GIFだと荒くてわかりにくいので画像置いておきます。
UIは適当に配置してるだけなのに、色があるだけでなんだか良さげ。


ライトモード

ライトモード

ライトモード

ダークモード

ダークモード

ダークモード

テーマ選択

終わりに

サンプルアプリのアイコンはこちらからお借りいたしました。
https://spy-family.net/special/special1.php

犬派なので推しはボンドです。

アプリを閉じてもテーマ設定を維持したい場合は、
ローカルDBにても値をキャッシュしておいて取り出せばいいので簡単です。

GitHub にコード置いてますのでぜひ動かしてみてください。
https://github.com/nemototea/compose-theming-sample

Discussion