Contents
Jetpack Compose のテーマ基礎と MaterialTheme の構造
Compose では UI コンポーネントが内部で MaterialTheme が提供するオブジェクトを参照して描画されます。これらを適切に設定すれば、個々のビューを書き換えることなく全体の外観を一括変更でき、保守性とデザインの一貫性が大幅に向上します。
ColorScheme・Typography・Shapes の役割とデフォルト設定
ColorScheme はアプリ全体で使用する色セット、Typography は文字スタイル、Shapes は角丸やボーダー形状を定義します。以下のコードは Material3 用の MaterialTheme を最小構成でラップした例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Composable fun AppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { // デフォルトの Light / Dark カラースキームを取得 val colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme() MaterialTheme( colorScheme = colorScheme, // 色セット typography = Typography(), // 文字スタイル(デフォルト) shapes = Shapes() // 形状(デフォルト) ) { content() } } |
- ポイント:
MaterialThemeの各パラメータは省略可能です。省略した場合はmaterial3ライブラリが提供する標準の Light / Dark スキームが自動適用されます。 - ベネフィット:カラーや文字・形状を上書きしたいときだけ該当オブジェクトを差し替えれば、他のコンポーネントは何も変更せずに新しいテーマを反映します。
Material You(動的カラー)の概要と利用条件
Material You は Android 12(API 31)以降で導入されたシステムレベルの配色機能です。端末が壁紙やアクセントカラーから自動生成した ColorScheme を取得でき、アプリは「OS のデザイン言語」に合わせて自然に外観を変化させられます。
動的カラーを取得する API と安全な呼び出し方
Compose が提供している関数は次の 2 つです。どちらも Context が必要ですが、API レベルが 31 未満の場合はデフォルトスキームを返すように設計されています。
| 関数 | 説明 |
|---|---|
dynamicLightColorScheme(context) |
Android 12+ のライトモード用動的カラー。API < 31 では lightColorScheme() を返す |
dynamicDarkColorScheme(context) |
Android 12+ のダークモード用動的カラー。API < 31 では darkColorScheme() を返す |
使用例(安全に呼び出すラッパー)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Composable fun DynamicAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { // Android12 以上かどうかをビルド時に判定 val useDynamic = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val colorScheme = when { useDynamic && darkTheme -> dynamicDarkColorScheme(LocalContext.current) useDynamic && !darkTheme -> dynamicLightColorScheme(LocalContext.current) darkTheme -> darkColorScheme() else -> lightColorScheme() } MaterialTheme(colorScheme = colorScheme, typography = Typography(), shapes = Shapes()) { content() } } |
- ポイント:
dynamicColorというパラメータはMaterialThemeに存在しません。動的カラーの有無は自前で Boolean 判定してから上記関数を呼び出す形が正しい実装です。
デバイス別の挙動とフォールバック戦略
- Android 12(API 31)以降:
dynamic*ColorSchemeがシステム生成カラーを返し、壁紙変更に即座に追従します。 - Android 11 以下:同関数は内部で
lightColorScheme()/darkColorScheme()を返すため、アプリ側で例外処理を書く必要はありません。 - フォールバック実装のベストプラクティス:上記ラッパーのように「API 判定 → 動的カラー取得 → デフォルト」 のフローを一箇所に集約すれば、各画面で同様のコードを書く手間が省けます。
カスタムテーマの作成と拡張テクニック
標準の ColorScheme だけではブランド固有の配色や独自トーンを表現しきれないケースがあります。そこで データクラス化したカラーパレット と MaterialTheme の拡張関数 を組み合わせる手法をご紹介します。
独自 ColorPalette の定義と拡張関数による利用例
まずはアプリ独自のカラーセットを data class で表現し、暗色・明色それぞれにインスタンスを用意します。次に MaterialTheme に拡張プロパティを追加することで、Compose の内部から簡潔に参照できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// ui/theme/CustomPalette.kt package com.example.coreui.theme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.material3.MaterialTheme import androidx.compose.foundation.isSystemInDarkTheme data class CustomColors( val brandPrimary: Color, val brandSecondary: Color, val errorContainer: Color ) private val LightPalette = CustomColors( brandPrimary = Color(0xFF0061A4), brandSecondary = Color(0xFF00BFA5), errorContainer = Color(0xFFFFEBEE) ) private val DarkPalette = CustomColors( brandPrimary = Color(0xFF90CAF9), brandSecondary = Color(0xFF80CBC4), errorContainer = Color(0xFFCF6679) ) // MaterialTheme に拡張プロパティを追加 val MaterialTheme.customColors: CustomColors @Composable get() = if (isSystemInDarkTheme()) DarkPalette else LightPalette |
カスタムカラーの利用例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Composable fun BrandButton( onClick: () -> Unit, text: String ) { Button( onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.customColors.brandPrimary) ) { Text(text, color = Color.White) } } |
- ポイント:
MaterialTheme.customColorsは@Composableプロパティとして提供されるため、テーマ切替(ライト/ダーク)に自動追従します。コードベースが一元化され、カラー変更時の影響範囲を最小限に抑えられます。
Typography と Shapes の上書き方法
文字スタイルと形状も同様にプロジェクト全体で統一したい場合は、Typography と Shapes のインスタンスを自前で作成し、Theme.kt へ注入します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// ui/theme/Type.kt private val AppFontFamily = FontFamily( Font(R.font.roboto_regular, FontWeight.Normal), Font(R.font.roboto_medium, FontWeight.Medium), Font(R.font.roboto_bold, FontWeight.Bold) ) val AppTypography = Typography( displayLarge = TextStyle( fontFamily = AppFontFamily, fontWeight = FontWeight.Bold, fontSize = 30.sp ), bodyMedium = TextStyle( fontFamily = AppFontFamily, fontWeight = FontWeight.Normal, fontSize = 16.sp ) // 必要に応じて他のスタイルも上書き ) // ui/theme/Shape.kt val AppShapes = Shapes( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(8.dp), large = RoundedCornerShape(12.dp) ) |
Theme.kt に組み込むだけで、全コンポーネントが新しい書体・形状を自動的に使用します。
マルチモジュールプロジェクトでのテーマ一元管理
大規模アプリでは UI 関連コードを core-ui といった共通ライブラリに集約し、各 Feature モジュールから参照させる構成が推奨されます。これによりテーマ変更時の影響範囲が限定され、ビルド時間や依存関係の管理もシンプルになります。
Theme.kt の構成例と core-ui モジュールへの切り出し方
以下は core-ui ライブラリ内に配置した Theme.kt の実装例です。動的カラー対応、カスタムパレット、Typography・Shapes のすべてを一箇所で定義しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// core-ui/src/main/kotlin/com/example/coreui/theme/Theme.kt package com.example.coreui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.material3.MaterialTheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme @Composable fun AppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { // Android12+ かどうかで動的カラー取得を切り替える val useDynamic = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val colorScheme = when { useDynamic && darkTheme -> dynamicDarkColorScheme(LocalContext.current) useDynamic && !darkTheme -> dynamicLightColorScheme(LocalContext.current) darkTheme -> darkColorScheme() else -> lightColorScheme() } MaterialTheme( colorScheme = colorScheme, typography = AppTypography, shapes = AppShapes ) { content() } } |
Gradle 設定例(マルチモジュール構成)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// settings.gradle.kts include(":app", ":core-ui", ":feature-login", ":feature-home") // core-ui/build.gradle.kts plugins { id("com.android.library") kotlin("android") } android { namespace = "com.example.coreui" compileSdk = 34 defaultConfig { minSdk = 21 } } dependencies { implementation(libs.compose.ui) implementation(libs.compose.material3) } |
各 Feature モジュールは implementation(project(":core-ui")) を追加すれば、AppTheme とカスタムカラーを即座に利用できます。
ランタイムでのテーマ切替とパフォーマンス最適化
ユーザーがライト/ダークや独自カラーパレットを選択できるようにするには、State 管理 と リコンポジション削減 が重要です。以下では ViewModel と DataStore を組み合わせた永続化手法と、テーマオブジェクトの remember キャッシュによる高速描画テクニックを紹介します。
State 管理によるテーマ選択の永続化
DataStore に保存した設定値を ViewModel が Flow で購読し、Compose 側では collectAsStateWithLifecycle と rememberSaveable を併用して UI に反映させます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// ui/theme/ThemePreference.kt enum class ThemeMode { LIGHT, DARK, SYSTEM } @Composable fun rememberAppTheme( viewModel: ThemeViewModel = androidx.lifecycle.viewmodel.compose.viewModel() ): Pair<ThemeMode, (ThemeMode) -> Unit> { val mode by viewModel.themeMode.collectAsStateWithLifecycle() val setMode: (ThemeMode) -> Unit = { viewModel.setThemeMode(it) } // rememberSaveable で再起動後も保持 val current = rememberSaveable { mutableStateOf(mode) } return current.value to setMode } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// ViewModel 側 class ThemeViewModel( private val dataStore: DataStore<Preferences> ) : ViewModel() { private val _themeMode = MutableStateFlow(ThemeMode.SYSTEM) val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow() init { restoreFromDataStore() } private fun restoreFromDataStore() { viewModelScope.launch { dataStore.data.map { prefs -> ThemeMode.valueOf(prefs[KEY_THEME] ?: ThemeMode.SYSTEM.name) }.collect { _themeMode.value = it } } } fun setThemeMode(mode: ThemeMode) { viewModelScope.launch { dataStore.edit { it[KEY_THEME] = mode.name } _themeMode.value = mode } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Composable fun MyApp() { val (mode, setMode) = rememberAppTheme() val darkTheme = when (mode) { ThemeMode.LIGHT -> false ThemeMode.DARK -> true ThemeMode.SYSTEM -> isSystemInDarkTheme() } AppTheme(darkTheme = darkTheme) { // アプリ全体の NavHost などを配置 } } |
- ポイント:
rememberSaveableによりプロセス再起動後も選択状態が保持され、ユーザーエクスペリエンスが向上します。
テーマオブジェクトの remember キャッシュとプレビュー活用
テーマオブジェクトは不変(immutable)であることが理想です。colorScheme の生成コストを削減するために、darkTheme・dynamicColor が変化した時だけ再計算するよう remember でラップします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@Composable fun AppTheme( darkTheme: Boolean, content: @Composable () -> Unit ) { val useDynamic = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S // 色セットのキャッシュ val colorScheme = remember(darkTheme, useDynamic) { when { useDynamic && darkTheme -> dynamicDarkColorScheme(LocalContext.current) useDynamic && !darkTheme -> dynamicLightColorScheme(LocalContext.current) darkTheme -> darkColorScheme() else -> lightColorScheme() } } MaterialTheme( colorScheme = colorScheme, typography = AppTypography, // immutable なので remember 不要 shapes = AppShapes ) { content() } } |
Compose Preview と Palette Tool の併用
- Preview:
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)/NOを付与すれば、ライトとダークの両方でテーマがどのように見えるか即座に確認できます。 -
Palette Tool:Android Studio の「Palette」パネルは現在適用されている
ColorSchemeの色を一覧表示し、カスタムカラーとの相性を視覚的に評価できる便利なツールです。 -
ベネフィット:開発中に実機やエミュレータでビルド・起動する手間が減り、デザインの微調整サイクルが大幅に短縮されます。
まとめ
- テーマの核は
ColorScheme·Typography·Shapes。MaterialTheme に集約すれば全画面で一貫した UI が実現できます。 - Material You(動的カラー) は Android 12+ のみ有効で、
dynamic*ColorScheme系 API を安全にラップして使用します。dynamicColorパラメータは存在しません。 - カスタムパレットは data class と拡張関数で管理すると型安全かつ再利用性が向上します。
- マルチモジュール構成では core-ui にテーマコードを集約し、各 Feature が
implementation(project(":core-ui"))だけで共通デザインを継承できます。 - ランタイム切替は ViewModel + DataStore + rememberSaveable の組み合わせが推奨され、永続化と即時反映が可能です。
- パフォーマンス最適化は
rememberキャッシュと Android Studio の Palette/Preview 活用で実現し、不要なリコンポジションを防ぎます。
本稿のコードや最新サンプルは以下のリポジトリで確認できます。
🔗 https://github.com/yourname/compose-theme-sample
ぜひ自プロジェクトに取り入れ、保守性とデザイン品質の高い Compose アプリを構築してください。