Open1

Jetpack Compose でテーマカラーを DI したい

うえむーうえむー

Jetpack Compose でテーマカラーを DI で差し替えたい場合にどのような方法がとれるか。

条件

  • Theme は CompositionLocal で提供できること
  • Preview がレンダリングできること
// カラーモデル
@Immutable
data class CustomColors(
    val color: Color,
    ...
)

// CompositionLocal を準備する
// 頻繁に変更はないことを想定して static を使用する
val LocalCustomColors = staticCompositionLocalOf<CustomColors> { "error("No LocalColors provided")" }

// テーマを提供するスコープを作成する
@Composable
fun CustomTheme(
    // 初期値を null にすることで Preview で楽をする
    colors: CustomColors? = null,
    content: @Composable () -> Unit,
) {
    CompositionLocalProvider(
        LocalCustomColors provides (colors ?: defaultColors),
        content = content,
    )
}

object CustomTheme {
    val colors: CustomColors
        @Composable get() = LocalCustomColors.current
}

// Composable 関数に対して DI できるように EntryPoint を定義する
@EntryPoint
@InstallIn(ActivityComponent::class)
interface DesignSystemEntryPoint {
    val colors: CustomColors
}

@Suppress("unused")
@Module
@InstallIn(SingletonComponent::class)
object DesignSystemModule {
        @Provides
        fun provideCustomColors(): CustomColors = ... // カラーのインスタンスを提供します
}

// Context から Activity を取得する
tailrec fun Context.findActivity(): ComponentActivity? {
    if (this is ComponentActivity) return this

    return (this as? ContextWrapper)?.baseContext?.findActivity()
}

fun Context.requireActivity(): ComponentActivity = checkNotNull(findActivity())

Hilt の EntryPoint は Compose を使う上でも非常に強力なツールですね。
つづいて使い方。

// In Composable
val entryPoint: DesignSystemEntryPoint = remember(context) {
    EntryPointAccessors.fromActivity(context.requireActivity())
}

CustomTheme(
    // DI で取得したカラーが適応される
    colors = designSystemEntryPoint.colors,
) {
    // you can use color getting by CustomTheme.
    // val color = CustomTheme.colors.color
}

// In Preview
@Preview
@Composable
fun ComponentPreview() {
    // 引数を指定しなければ初期値でレンダリングされる
    CustomTheme {
        // you can use color getting by CustomTheme.
        // val color = CustomTheme.colors.color
    }
}