スタイルによるテーマ設定

スタイルを使用してアプリを構築する方法はいくつかあります。どの方法を選択するかは、マテリアル デザインの採用状況に応じて異なります。

  1. マテリアル デザインを使用しないフルカスタム デザイン システム
    • 推奨事項: テーマから値を使用するコンポーネント スタイルを定義し、デザイン システム コンポーネントでスタイル パラメータを公開します。
  2. マテリアル デザインを使用する
    • 推奨事項: マテリアルが採用されるまで待ってから、スタイルと統合します。 可能な場合は、独自のコンポーネントでスタイルを使用します。

スタイル レイヤ

従来の Compose モデルでは、カスタマイズは多くの場合、MaterialTheme によって提供されるグローバル トークン(色とタイポグラフィ)のオーバーライドや、可能な場合はデザイン システムのコンポーザブルのプロパティのラッピングとオーバーライドに大きく依存しています。サブシステムやパラメータを介して公開されていないプロパティがマテリアル レイヤ内に存在し、コンポーネント自体にハードコードされたデフォルト値になっている場合もあります。

Styles API には、サブシステムとコンポーネントの橋渡しとなる新しい抽象化レイヤ(スタイル )があります。

レイヤ 責任範囲
サブシステムの値 名前付きの値 val Primary = Color(0xFF34A85E)
アトミック スタイル 1 つのプロパティ変更のみを行うスタイル val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic
コンポーネント スタイル コンポーネント固有の構成 プライマリ背景と 16dp のパディングを持つボタン。val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
コンポーネント スタイルを使用する機能的な UI 要素。 Button(style = buttonStyle) { ... }
新しいレイヤの導入によるスタイルを使用したテーマ設定を示す図
図 1.コンポーネントの例と、テーマからスタイルにアクセスする方法。

アトミック スタイルとモノリシック スタイル

Styles API を使用すると、スタイルを個別の原子スタイルに分解できます。 baseButtonStyle のような複雑なコンポーネント固有のスタイルを定義する代わりに、小規模で単一目的のユーティリティ スタイルを作成することもできます。これらは「原子」として機能します。

// Define single-purpose "atomic" styles
val paddingAtomic = Style {
    contentPadding(16.dp)
}
val roundedCornerShapeAtomic = Style {
    shape(RoundedCornerShape(8.dp))
}
val primaryBackgroundAtomic = Style {
    background(Color.Blue)
}
val largeSizeAtomic = Style {
    size(100.dp, 40.dp)
}
val interactiveShadowAtomic = Style {
    hovered {
        animate {
            dropShadow(
                Shadow(
                    offset = DpOffset(
                        0.dp,
                        0.dp
                    ),
                    radius = 2.dp,
                    spread = 0.dp,
                    color = Color.Blue,
                )
            )
        }
    }
}

「then」を使用した構成

新しい Styles API の強力な機能の 1 つに then 演算子があります。この演算子を使用すると、複数の Style オブジェクトをマージできます。これにより、アトミック ユーティリティ クラスを使用してコンポーネントを構築できます。

従来型(非アトミック):

// One large monolithic style
val buttonStyle = Style {
    contentPadding(16.dp)
    shape(RoundedCornerShape(8.dp))
    background(Color.Blue)
}

アトミック リファクタリング:

// Combine atoms to create the final appearance
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic

デザイン システムにスタイルを採用する

デザイン システムにスタイルを採用する場合は、デザイン システムがスペクトラムのどの位置にあるかに応じて、次のオプションを検討してください。

スタイルを使用したカスタム デザイン システム

検討すべき場合: マテリアル デザインに基づいていない包括的なブランドガイドが提供されており、マテリアル デザインを使用する予定がない場合

**戦略**: フルカスタム デザイン システムを実装し、スタイルをテーマの一部 として公開します。

マテリアルをメインのデザイン システム言語として使用しない場合は、カスタムパスを使用します。ビジュアル定義には MaterialTheme を完全にバイパスし、 独自のカスタム テーマをすでに作成しています。スタイルのコンテナとして機能する CompanyTheme を構築します。

  • 仕組み: システム内のすべてのコンポーネントの Style オブジェクト を保持する CompanyTheme オブジェクトを作成します。コンポーネント(マテリアル ロジックのラッパー、カスタムの Box 実装、Layout 実装)はこれらのスタイルを直接使用し、デザイン システムのコンシューマに対して Style パラメータを公開します。
  • スタイル レイヤ: スタイルはデザイン システム の主要な定義です。トークンは、これらのスタイルに渡される名前付き変数です。これにより、状態変化に対する一意のアニメーション(押下時のスケールと色の変化など)の定義など、詳細なカスタマイズが可能になります。

マテリアルを使用せずに独自のカスタムテーマを構築していて、 スタイルを採用する場合は、スタイルのリストをテーマに追加します。これにより、プロジェクト内のどこからでも基本スタイルにアクセスできます。

  1. アプリケーション内のさまざまなスタイルを保存し、デフォルトを作成する Styles クラスを作成します。たとえば、Jetsnack アプリでは、クラス名は JetsnackStyles です。

    object JetsnackStyles{
        val buttonStyle: Style = Style {
            shape(shapes.medium)
            background(colors.brand)
            contentColor(colors.textPrimary)
            contentPaddingVertical(8.dp)
            contentPaddingHorizontal(24.dp)
            textStyle(typography.labelLarge)
            disabled {
                animate {
                    background(colors.brandSecondary)
                }
            }
        }
        val cardStyle: Style = Style {
            shape(shapes.medium)
            background(colors.uiBackground)
            contentColor(colors.textPrimary)
        }
    }

  2. Styles を全体的なテーマの一部として提供し、サブシステムにアクセスするためのヘルパー拡張関数を StyleScope で公開します。

    @Immutable
    class JetsnackTheme(
        val colors: JetsnackColors = LightJetsnackColors,
        val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(),
        val shapes: Shapes = Shapes()
    ) {
        companion object {
            val colors: JetsnackColors
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.colors
    
            val typography: androidx.compose.material3.Typography
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.typography
    
            val shapes: Shapes
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.shapes
    
            val styles: JetsnackStyles = JetsnackStyles
    
            val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme>
                get() = LocalJetsnackThemeInstance
        }
    }
    
    val StyleScope.colors: JetsnackColors
        get() = LocalJetsnackTheme.currentValue.colors
    
    val StyleScope.typography: androidx.compose.material3.Typography
        get() = LocalJetsnackTheme.currentValue.typography
    
    val StyleScope.shapes: Shapes
        get() = LocalJetsnackTheme.currentValue.shapes
    
    internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() }
    
    @Composable
    fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
        val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors
        val theme = JetsnackTheme(colors = colors)
    
        CompositionLocalProvider(
            LocalJetsnackTheme provides theme,
        ) {
            MaterialTheme(
                typography = LocalJetsnackTheme.current.typography,
                shapes = LocalJetsnackTheme.current.shapes,
                content = content,
            )
        }
    }

  3. コンポーザブル内で JetsnackStyles にアクセスします。

    @Composable
    fun CustomButton(modifier: Modifier,
                     style: Style = Style,
                     text: String) {
        val interactionSource = remember { MutableInteractionSource() }
        val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    
        // Apply style to top level container in combination with incoming style from parameter.
        Box(modifier = modifier
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                enabled = true,
                role = Role.Button,
                onClick = {
    
                },
            )
            .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) {
            Text(text)
        }
    }

グローバル テーマの採用以外にも、アプリに Styles を組み込むための代替戦略があります。特定の呼び出しサイトで Styles をインラインで使用することも、完全なテーマ設定機能が不要な場合は静的定義を使用することもできます。 スタイル全体が根本的に異なる場合を除き、Styles を条件付きで切り替えることは避けてください。個別のスタイル オブジェクトを切り替えるのではなく、ビジュアル定義内で動的トークンにアクセスすることをおすすめします。