Jetpack Composeプロジェクトのテーマ指定

投稿日:  更新日:

「Android Studio Giraffe」の作成するプロジェクトは、Jetpack Composeの利用が推奨されます。

そして、作成されたプロジェクトは、Material Designeに準拠したテーマが指定されます。

※環境:Android Studio Giraffe | 2022.3.1 Patch 1
    Kotlin 1.8.10
    Compose Compiler 1.4.3

スポンサーリンク

テンプレートのUIツリー

テンプレートは図のようなUIツリー(階層化されたComposable関数)になっています。

サンプルのUIツリー

MainActivityファイル

そして、各々の階層は次のような役割を持っています。

  • (1)テーマの定義と選択(ライトorダーク)
  • (2)テーマの指定
  • (3)バックグラウンドの指定
  • (4)機能ブロックの定義
  • (5)”Hello Android”の表示

テーマの定義・選択・指定は、(1)、(2)の部分で行われています。
※「(数字)」は図中の「丸数字」に対応

スポンサーリンク

テーマの定義

テンプレートは、ダークテーマとライトテーマの2つがデフォルトで定義されています。

Material Designeの指標

Material Designeは、様々な表示要素のデザイン(美的な造形性)を決める指標と、その指標のルールを定義した仕様です。

Material Designeの指標は3つあります。

指標概要クラス
Color配色(Primary、Backgroundなど)ColorScheme
Typography文字の体裁(デザイン・配置、など) Typography
Shapes形状(角のまるみ、など) Shapes

各々の指標は専用のクラスを持ち、プロパティへ具体的な値を格納することで定義されます。

例えば、Color(ColorSchemeクラス)は次のようになっています。

@Stable
class ColorScheme(
    primary: Color,						// ... プロパティへ格納
    onPrimary: Color,	        // ... プロパティへ格納
    primaryContainer: Color,
    onPrimaryContainer: Color,
    inversePrimary: Color,
    secondary: Color,
    onSecondary: Color,
    secondaryContainer: Color,
    onSecondaryContainer: Color,
    tertiary: Color,
    onTertiary: Color,
    tertiaryContainer: Color,
    onTertiaryContainer: Color,
    background: Color,
    onBackground: Color,
    surface: Color,
    onSurface: Color,
    surfaceVariant: Color,
    onSurfaceVariant: Color,
    surfaceTint: Color,
    inverseSurface: Color,
    inverseOnSurface: Color,
    error: Color,
    onError: Color,
    errorContainer: Color,
    onErrorContainer: Color,
    outline: Color,
    outlineVariant: Color,
    scrim: Color,
) {
    var primary by mutableStateOf(primary, structuralEqualityPolicy())
        internal set
    var onPrimary by mutableStateOf(onPrimary, structuralEqualityPolicy())
        internal set
    ...
}

ダーク・ライトテーマ

テーマはMaterial Designeを使って、アプリ全体の見栄えを定義したものです。

テーマの定義はColorSchemeクラスへ色情報を格納することで行われます。

fun darkColorScheme(
    primary: Color = ColorDarkTokens.Primary,		// デフォルト値(定数)
    onPrimary: Color = ColorDarkTokens.OnPrimary,	// デフォルト値(定数)
	...
): ColorScheme =
    ColorScheme(
        primary = primary,
        onPrimary = onPrimary,
		...
	)

これを、ui.theme/Theme.ktでアプリ専用のダークテーマ(DarkColorScheme)へ改変しています。

private val DarkColorScheme = darkColorScheme(
    primary = Purple80,				// アプリ専用の色
    secondary = PurpleGrey80,		// 同上
	tertiary = Pink80				// 同上
)
val Purple80 = Color(0xFFD0BCFF)		// Dark用
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)

val Purple40 = Color(0xFF6650a4)		// Light用
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

ライトテーマ(LightColorScheme)も同様です。ここでは説明を省略します。

ダイナミックカラーテーマ

ダイナミックダークカラーテーマ(dynamicDarkColorScheme)の定義はDynamicTonalPalette.ktで行われています

@RequiresApi(Build.VERSION_CODES.S)
fun dynamicDarkColorScheme(context: Context): ColorScheme {
    val tonalPalette = dynamicTonalPalette(context)	// 壁紙に合わせた配色が返る
    return darkColorScheme(
        primary = tonalPalette.primary80,			// アプリ専用の色
        onPrimary = tonalPalette.primary20,			// 同上
        ...
    )
}

ライトテーマ(dynamicLightColorScheme)も同様です。ここでは説明を省略します。

ダイナミックカラー
「ダイナミックカラー」は2021年にGoogleが発表した「Material Youデザイン」の中核となる機能です。

Android 12(≧API 31)で登場しました。

今まで、アプリは開発側の方針で配色されてきました。「方針」には様々なものがあります。例えば、アプリの機能を連想させる色や、企業のブランドカラーなどです。

これに対し、「端末のユーザーがアプリの配色を決める機能」がダイナミックカラーです。

ダイナミックカラーに対応した全てのアプリの配色が変更されるので、端末をユーザの好みの色へカスタマイズできる点が売りのようです。

「どのくらいの需要があるか?!」は不明ですが …

具体的には、壁紙の色に合わせてテーマの配色が半自動的に決定されます。

例えば、図はAPI 33のSettingsアプリで、ランチャーの壁紙を変えた場合です。

壁紙を変えるページ(API33)
壁紙を変えるページ(API33)

壁紙が紫系⇒緑系に変わることで、テーマの配色(文字色)も同様に変更になり、複数の配色パターンから選択できるようになります。

なお、配色を決定する処理は「ダイナミックカラーフロー」と呼ばれ、システムに組み込まれています。

スポンサーリンク

テーマの選択

引数に指定されたフラグdarkThemeとdynamicColorにより、4つの中から1つテーマがcolorScheme変数へ選択されます。

フラグ
dynamicColor
false
true
darkTheme
false
lightColorScheme
dynamicLightColorScheme
true
darkColorScheme
dynamicDarkColorScheme

ただし、ダイナミックカラーはAPI≧31で有効です。

@Composable
fun MyApplicationTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) 
			else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = colorScheme.primary.toArgb()
            WindowCompat.getInsetsController(window, view)
			    .isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}
isSystemInDarkTheme( )

isSystemInDarkTheme()は、端末のテーマの状態(ダーク or ライト)を読み出します。

【isSystemInDarkTheme():true】

端末のテーマがダークである場合
【isSystemInDarkTheme():false】

端末のテーマがライトである場合

端末のテーマはSettingsアプリのDisplayページで指定されています。

スポンサーリンク

テーマの指定

選択されたテーマはMaterialTheme関数でCompositionLocalと呼ばれる特殊な変数(Mapのような変数)の中に格納されます。

※CompositionLocalの動作は「Jetpack Compose:UIツリーにローカルな変数の確保(CompositionLocal)」を参照

【CompositionLocal】
 LocalColorScheme  :Colorを格納
 LocalTypography   :Typographyを格納
 LocalShapes       :Shapesを格納
@Composable
fun MyApplicationTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) 
			else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            val window = (view.context as Activity).window
            window.statusBarColor = colorScheme.primary.toArgb()
            WindowCompat.getInsetsController(window, view)
			    .isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}
@Composable
fun MaterialTheme(
    colorScheme: ColorScheme = MaterialTheme.colorScheme,
    shapes: Shapes = MaterialTheme.shapes,
    typography: Typography = MaterialTheme.typography,
    content: @Composable () -> Unit
) {
    val rememberedColorScheme = remember {
        // Explicitly creating a new object here so we don't mutate the initial [colorScheme]
        // provided, and overwrite the values set in it.
        colorScheme.copy()
    }.apply {
        updateColorSchemeFrom(colorScheme)
    }
    val rippleIndication = rememberRipple()
    val selectionColors = rememberTextSelectionColors(rememberedColorScheme)
    CompositionLocalProvider(
        LocalColorScheme provides rememberedColorScheme,
        LocalIndication provides rippleIndication,
        LocalRippleTheme provides MaterialRippleTheme,
        LocalShapes provides shapes,
        LocalTextSelectionColors provides selectionColors,
        LocalTypography provides typography,
    ) {
        ProvideTextStyle(value = typography.bodyLarge, content = content)
    }
}

また、MaterialTheme関数が最初に呼ばれた時点で、MaterialThemeクラス(関数と同名)のインスタンスが作成されます。インスタンスはシングルトンです。

このMaterialThemeクラスはテーマの定義(Color、Typography、Shapes)を集約して管理するためのクラスです。

object MaterialTheme {
    /**
     * Retrieves the current [ColorScheme] at the call site's position in ...
     */
    val colorScheme: ColorScheme
        @Composable
        @ReadOnlyComposable
        get() = LocalColorScheme.current	// 定義を取り出す

    /**
     * Retrieves the current [Typography] at the call site's position in ...
     */
    val typography: Typography
        @Composable
        @ReadOnlyComposable
        get() = LocalTypography.current		// 定義を取り出す

    /**
     * Retrieves the current [Shapes] at the call site's position in ...
     */
    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = LocalShapes.current			// 定義を取り出す
}

アプリ内からテーマの定義を参照する場合は、このMaterialThemeクラスのインスタンを介して間接的に行います。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}
スポンサーリンク

関連記事:

プロジェクトのビルドで「Something went wrong while checking for version compatibility between the Compose Compiler and the Kotlin Compiler.」とメッセージを吐き、エラーになる場合があります。 既存のプロジェクトを新しくリリースされたAndroid Studioでビルドした場合に頻発します。 先日、「Giraffe|2022.3.1」がリリース(2023.07)されて、早速、ビルドをしたら発生しました。 その対処方法を説明します。 ※環境:Android Studio Giraffe | 2022.3.1 ...
表示の変わらないUI要素(Composable関数)に対して行われる再Composeは無駄な処理です。 ですので、極力排除したいところですが、表示が変わらないため、画面上からの判断が難しくなっています。 このようなとき、Layout Inspectorを利用すると、無駄な再Composeを見つけ出すことが出来ます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1 ...
Jetpack ComposeはAndroidシステムの新たなUIフレームワークです。従来のViewシステムと、アプリ画面の描画の仕組みが異なります。 このJetpack Composeによるアプリ画面の描画について、仕組みの大枠をまとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Jetpack Composeは描画処理の軽量化(消費リソース量の削減)をするために、表示の変更されたUI要素のみを再Composeし、表示の変わらないUI要素をスキップします。これにより、高い表示パフォーマンスを維持しています。 しかし、スキップが正常に行われないとしても、アプリの画面に現れて来ません。なぜなら、同じ表示を無駄に繰り返すことになるからです。 アプリは動くけれど動作が鈍いならば、真っ先に疑うポイントです。不要な再Composeが行われている可能性が考えられます。 これは気付かないうちに蓄積し易い不具合です。ですので、再Composeとスキップについて理解し、予防に努めることをお勧めします。 今回は「再Composeとスキップ」について、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
サンプルアプリを作成して、描画処理(再Compose)の周期を観測してみました。 その結果を紹介します。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
mutableStateOfはComposable関数ではありません。 ですので、Composable関数内にある必要はなく、どこでも記述できます。 Activityから表示の更新を発行する方法として使えそうです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
状態の保持(remenber)はアプリの画面(UI)を制御・管理するために必要な動作です。 再Composeのスケジューリング(mutableStateOf)に並び、Jetpack Composeの重要な技術の一つです。 今回は「再Composeを超えて状態の保持」について、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
再Composeのスケジューリング(mutableStateOf)はアプリのパフォーマンスに直結する動作です。 状態の保持(remember)に並び、Jetpack Composeの重要な技術の一つです。 今回は「再Composeのスケジューリング」について、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはUIツリーで発生する状態のバケツリレーを解決してくれます。 また、広く共有したい状態の定義にも適しています。例えば、「ツリーのある階層以下に対して」などと言った場合です。 CompositionLocalについて、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはUIツリーにローカルな変数を確保します。 その変数の参照キーはcompositionLocalOf関数(以降、Dynamic側と呼ぶ)によって返されるCompositionLocalインスタンスです。 このインスタンスを返す方法に、もう一つ、staticCompositionLocalOf関数(以降、Static側と呼ぶ)があります。 この両者の違いをまとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはシステムにより提供されているものがあります。 プログラミングに有益で利用頻度の高いものが用意されています。例えば、LocalContextやLocalConfigurationなどです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
「Android Studio Giraffe」の作成するプロジェクトは、Jetpack Composeの利用が推奨されます。 今後、Viewシステムに代わり、Jetpack Composeが主流になるようです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Android StudioにおけるJetpack Composeプロジェクトは、エディタ上でUIのプレビューが行えます。 Kotlinで記述した画面構成(UIツリー)が視覚的に確認できるので、とても便利です。 さらに、色々な表示条件の設定が行えるので、使いこなせば更に利便性が向上します。 ※この記事の執筆中にドキュメント「コンポーザブルのプレビューで UI をプレビューする」を見つけました。記事はこのドキュメントと重複する部分が多いです。ドキュメントも参考にして下さい。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 3     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Jetpack Composeが提供する既存のUI要素(Compose UI)は、必ずModifierを引数に持ちます。 このModifierの役割はUI要素へ装飾や機能拡張を追加することですが、裏でアプリ画面の描画処理と密接に関連しており、UI要素よりもシステム側に近い存在です。 理解せずに誤った使い方をすれば、装飾や機能拡張の域を脱してUI要素が表示されないこともあり、思ったようなアプリ画面は望めません。 Modifierについて、まとめます。 ※環境:Android Studio Hedgehog | 2023.1.1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
スポンサーリンク