🍣

JetpackComposeでデザインシステムを構築する

2021/10/08に公開

※本記事はこちらの記事を読みやすくしたものと、ちょいプラスαした記事です。

公式ライブラリで作成する方法の記事も投稿しました。

https://zenn.dev/apple_nktn/articles/fbf6191d83c078

そもそもマテリアルデザインがあるので0から構築するのはやめよう

タイトルのまんまで車輪の再開発をしてもしょうがないので、

マテリアルデザインを流用し、

必要な箇所をオーバーライドして使うのがスマートである。

実装

カラーパレットを作成して、ブランドデザインの基盤を作る。

object DlsColors 
{    val primary = Color(0xFF3366FF)
    val background = Color(0xFFFFFFFF)
    val backgroundReverse = Color(0xFF192038)
    val basic = Color(0xFF8F9BB3)
    val disable = basic.copy(alpha = 0.24f)
    val text = Color(0xFF192038)
    val textReverse = Color(0xFFFFFFFF)
    val success = Color(0xFF00E096)
    val link = Color(0xFF0095FF)
    val warning = Color(0xFFFFAA00)
    val error = Color(0xFFFF3D71)}

カラーパレットのインターフェースを定義する。

materialColors は MaterialTheme の色をオーバーライドするために作る。

interface DlsColorPalette {
    val primary: Color
    val background: Color
    val basic: Color
    val disable: Color
    val text: Color
    val success: Color
    val link: Color
    val warning: Color
    val error: Color
    val materialColors: Colors}

定義したカラーパレットを実装する。

dlsLightColorPalette はデザインシステムのカラーパレットとカスタマイズされた MaterialTheme カラーを返すようになる。

fun dlsLightColorPalette(): DlsColorPalette = object : DlsColorPalette {
    override val primary: Color = DlsColors.primary
    override val background: Color = DlsColors.background
    override val basic: Color = DlsColors.basic
    override val disable: Color = DlsColors.disable
    override val text: Color = DlsColors.text
    override val success: Color = DlsColors.success
    override val link: Color = DlsColors.link
    override val warning: Color = DlsColors.warning
    override val error: Color = DlsColors.error
    override val materialColors: ColorPalette = lightColorPalette(
        primary = DlsColors.primary
    )}

MaterialTheme のカラーをオーバーライドすることは必須ではなく、

オーバーライドすることでアプリの全体にわたる色の指定が簡単にでき、

UI 実装時に色を指定するコードを減らすことができる。

ダークモード用のカラーパレットを作る

fun dlsDarkColorPalette(): DlsColorPalette = object : DlsColorPalette {
    override val primary: Color = DlsColors.primary
    override val background: Color = DlsColors.backgroundReverse
    override val basic: Color = DlsColors.basic
    override val disable: Color = DlsColors.disable
    override val text: Color = DlsColors.textReverse
    override val success: Color = DlsColors.success
    override val link: Color = DlsColors.link
    override val warning: Color = DlsColors.warning
    override val error: Color = DlsColors.error
    override val materialColors: Colors = darkColors(
        primary = DlsColors.primary
    )}

サイズ指定

デバイス画面のサイズによってサイズを変更するといった、

複数のバリエーションを持つ必要がある場合は、

カラーパレットと同様の方法で対応可能

data class DlsSize internal constructor(
    val smaller: Dp = 4.dp,
    val small: Dp = 8.dp,
    val medium: Dp = 16.dp,
    val large: Dp = 32.dp,
    val larger: Dp = 64.dp
    )

Typography

タイポグラフィもサイズの定義と同様に定義する。

テーマに応じてフォントを使い分ける必要がある場合、

また言語によって使用するフォントを切り替えたい場合など、

複数のフォントバリアントのサポートが必要な場合、

その場合は配色と同様に実装することで対応可能。

@Immutable
data class DlsTypography internal constructor(
    val headline1: TextStyle = TextStyle(
        fontSize = 36.sp,
        fontWeight = FontWeight.Bold,
        lineHeight = 48.sp
    ),
    
    ......
    
    val button: TextStyle = TextStyle(
        fontSize = 14.sp,
        fontWeight = FontWeight.Bold,
        lineHeight = 16.sp
    ),

    val materialTypography: Typography = Typography(
        body1 = paragraph1
    ))

materialTypography は、

materialColors と同様、MaterialTheme のフォントをオーバーライドするために用意している。

テーマ(ブランド)

MaterialTheme を利用することで、

Material Design に対応した UI を作成できる。

しかし今作成しているデザインシステムは

Material Design をベースにしたものではないため、

独自の テーマ(ブランド) を追加する必要がある。

@Composable
fun DlsTheme(
    colors: DlsColorPalette = dlsLightColorPalette(),
    typography: DlsTypography = DlsTypography(),
    children: @Composable() () -> Unit) {
    CompositionLocalProvider(
        LocalDlsColors provides colors,
        LocalDlsTypography provides typography,
    ) {
        MaterialTheme(
            colors = colors.materialColors,
            typography = typography.materialTypography
        ) {
            children()
        }
    }}

object DlsTheme {
    val colors: DlsColorPalette
        @Composable
        @ReadOnlyComposable
        get() = LocalDlsColors.current

    val typography: DlsTypography
        @Composable
        @ReadOnlyComposable
        get() = LocalDlsTypography.current

    val sizes: DlsSize
        @Composable
        @ReadOnlyComposable
        get() = DlsSize()}

internal val LocalDlsColors = staticCompositionLocalOf { dlsLightColorPalette() }
internal val LocalDlsTypography = staticCompositionLocalOf { DlsTypography() }

コンポーザブル関数 DlsTheme は、

テーマ(ブランド)によって変化する可能性のあるスタイルを外から受け取流ことができる。

今作成しているデザインシステムの場合、

配色と、タイポグラフィ を受け取ることが可能。

DlsTheme は MaterialTheme をラップしている。

Material Design の機能とスタイルを

デザインのベースとして流用することで、

デザインシステムの実装コストを大幅に減らすことができている。

CompositionLocalProviderとstaticCompositionLocalOf

CompositionLocalProvider でプロバイドしたスタイルバリュー

(サンプルでは Color とTypography)は

CompositionLocalProvider の下の階層のどこでも

CompositionLocal.current
(LocalDlsColors.current と LocalDlsTypography.current)

でアクセスすることができる。

そのバリューを変更すると CompositionLocal.current も自動的に更新される。

使い方

DlsTheme {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = DlsTheme.colors.background
    ) {
          Text(
              text = "TEXT",
              color = DlsTheme.colors.success,
              style = DlsTheme.typography.paragraph1
          )
    }}

まず、

デザインシステムを適用したい画面を DlsTheme でラップする。

必要なスタイルは DlsTheme からアクセスでき、

MaterialTheme の使い方は変わらない。

ダークモードをスイッチ対応する場合

ダークモードなどの違うテーマをサポートするときは、

DlsTheme にスタイルを渡すことで実現可能。

val isDarkState = mutableStateOf(false)
setContent {
    DlsTheme(
        colors = if (isDarkState.value) dlsDarkColorPalette() else dlsLightColorPalette()
    ) {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = DlsTheme.colors.background
        ) {
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = if (isDarkState.value) "Is Dark" else "Is Light",
                    color = DlsTheme.colors.text,
                    style = DlsTheme.typography.paragraph1
                )

                Spacer(modifier = Modifier.height(DlsTheme.sizes.medium))

                Button(
                    onClick = {
                        isDarkState.value = !isDarkState.value
                    }
                ) {
                    Text(
                        text = text,
                        style = DlsTheme.typography.button
                    )
                }
            }
        }
    }}

システムのダークモードに合わせて変化させる場合

@Composable
fun DlsTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    typography: DlsTypography = DlsTypography(),
    children: @Composable() () -> Unit) {
    val colors = if (darkTheme) dlsDarkColorPalette()
                 else dlsLightColorPalette()
    CompositionLocalProvider(
        LocalDlsColors provides colors,
        LocalDlsTypography provides typography,
    ) {
        MaterialTheme(
            colors = colors.materialColors,
            typography = typography.materialTypography
        ) {
            children()
        }
    }}

object DlsTheme {
    val colors: DlsColorPalette
        @Composable
        @ReadOnlyComposable
        get() = LocalDlsColors.current

    val typography: DlsTypography
        @Composable
        @ReadOnlyComposable
        get() = LocalDlsTypography.current

    val sizes: DlsSize
        @Composable
        @ReadOnlyComposable
        get() = DlsSize()}

internal val LocalDlsColors = staticCompositionLocalOf { dlsLightColorPalette() }
internal val LocalDlsTypography = staticCompositionLocalOf { DlsTypography() }

参考記事

https://developer.android.com/guide/topics/ui/look-and-feel/themes?hl=ja

https://android-developers-jp.googleblog.com/2021/05/implementing-design-system-using-jetpack-compose.html

https://medium.com/mobile-app-development-publication/android-jetpack-compose-theme-made-easy-1812150239fe

https://developer.android.com/jetpack/compose/compositionlocal?hl=ja

https://qiita.com/ue-moo/items/0b407dc4e4f571a52d8d

http://y-anz-m.blogspot.com/2021/04/jetpack-compose-composition-local.html

Discussion