Compose Multiplatform 実践:CMPのComposeでMaterial Design3 Themeを使用する
はじめに
Compose Multiplatform (略称CMP)
今日は
CMPの共通ロジックでMaterial Design3 Theme (またはMaterial 3と呼ばれる)を使用する方法と
Compose UIでMaterial 3を使用してアプリケーションUIを構築する方法について説明します
Material Design3 Theme
Compose MultiplatformにMaterial Design3 Theme (Material 3)を適用することは
直感的で一貫性のあるユーザーインターフェースを構築する重要なステップです
Material 3はGoogleが発表したデザインガイドラインで
ユーザーエクスペリエンスを向上させることを目的とした
新しいデザイン原則のセットを提供しています
-
カスタム関数
ElegantAccessComposeTheme { }を作成しますCompose要素を記述済みの
funtion typeまたはlambda functionに配置するだけでカスタマイズした
Material 3 Themeを適用できますこれによりUI themeの設定を統一的に管理できます
// in .~/commonMain/..
@OptIn(KoinExperimentalAPI::class)
@Composable
@Preview
fun App() {
//ElegantAccessComposeThemeを通じてMaterial 3テーマを設定
ElegantAccessComposeTheme {
val viewModel = koinViewModel<MainViewModel>()
//`ElegantAccessApp`は任意のカスタムCompose要素
ElegantAccessApp(viewModel)
}
}
-
ステップ1. Material 3テーマのインポート
CMPプロジェクトのbuild.gradle.ktsでMaterial 3ライブラリをインポートします
build.gradle.ktsファイルのcommonMainセクションで関連する依存関係を設定できます:
sourceSets {
commonMain.dependencies {
implementation(compose.material3)
}
}
-
ステップ2. Material 3テーマの実装
Kotlinの特性である
function typeを活用して関数を作成しこの関数を適用するだけでUI共通ルールを設定できるようにします
以下のようなコードを記述します
ここでは一般的に使用されるUI Designsを含めています
ダークモード、ステータスバーの色、共通Materialテーマなどを含みます
// in .~/commonMain/..
@Composable
fun ElegantAccessComposeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
darkTheme -> EADarkColorScheme()
else -> EALightColorScheme()
}
setStatusBarStyle(
backgroundColor = colorScheme.background,
isDarkTheme = darkTheme
)
MaterialTheme(
colorScheme = colorScheme,
typography = createTypography(colorScheme),
shapes = shapes,
content = content
)
}
コードの重要な部分の説明:
-
関数内でcomposeが提供する
isSystemInDarkTheme()を使用した変数を導入しダークモードかどうかを直接判断します
-
content: @Composable () -> Unitは関数型変数を宣言し開発者が外部からfunction type
{}の内容を渡せるようにします -
colorScheme:ここではダークモードかどうかを判断し、対応する色のスキームを返します -
setStatusBarStyle(backgroundColor, isDarkTheme):CMPなので、各プラットフォームでステータスバーの色を設定できるようにexpect funを作成しました -
MaterialTheme(colorScheme, typography, shapes, content):ここではMaterial3の組み込み関数を呼び出してテーマカラー、フォント、要素の形などを設定します
-
ステップ3. colorSchemeの実装
ここではComposeが提供する
darkColorSchemeまたはlightColorSchemeを使用してダークモードまたはライトモードで使用するColorを設定します
ダークモード
// in .~/commonMain/..
@Composable
private fun EADarkColorScheme() = darkColorScheme(
primary = ColorResources.Dark.primary,
onPrimary = ColorResources.Dark.onPrimary,
primaryContainer = ColorResources.Dark.primaryContainer,
onPrimaryContainer = ColorResources.Dark.onPrimaryContainer,
secondary = ColorResources.Dark.secondary,
onSecondary = ColorResources.Dark.onSecondary,
tertiary = ColorResources.Dark.tertiary,
background = ColorResources.Dark.background,
onBackground = ColorResources.Dark.onBackground,
surface = ColorResources.Dark.surface,
onSurface = ColorResources.Dark.onSurface,
surfaceVariant = ColorResources.Dark.surfaceVariant,
onSurfaceVariant = ColorResources.Dark.onSurfaceVariant,
surfaceDim = ColorResources.Dark.surfaceDim,
error = ColorResources.Dark.error,
onError = ColorResources.Dark.onError,
outlineVariant = ColorResources.Dark.outlineVariant,
surfaceContainerHigh = ColorResources.Dark.surfaceContainerHigh,
surfaceContainer = ColorResources.Dark.surfaceContainer,
surfaceContainerLow = ColorResources.Dark.surfaceContainerLow,
inverseSurface = ColorResources.Dark.inverseSurface
)
ライトモード
// in .~/commonMain/..
@Composable
private fun EALightColorScheme() = lightColorScheme(
primary = ColorResources.Light.primary,
onPrimary = ColorResources.Light.onPrimary,
primaryContainer = ColorResources.Light.primaryContainer,
onPrimaryContainer = ColorResources.Light.onPrimaryContainer,
secondary = ColorResources.Light.secondary,
onSecondary = ColorResources.Light.onSecondary,
tertiary = ColorResources.Light.tertiary,
background = ColorResources.Light.background,
onBackground = ColorResources.Light.onBackground,
surface = ColorResources.Light.surface,
onSurface = ColorResources.Light.onSurface,
surfaceVariant = ColorResources.Light.surfaceVariant,
onSurfaceVariant = ColorResources.Light.onSurfaceVariant,
surfaceDim = ColorResources.Light.surfaceDim,
error = ColorResources.Light.error,
onError = ColorResources.Light.onError,
outlineVariant = ColorResources.Light.outlineVariant,
surfaceContainerHigh = ColorResources.Light.surfaceContainerHigh,
surfaceContainer = ColorResources.Light.surfaceContainer,
surfaceContainerLow = ColorResources.Light.surfaceContainerLow,
inverseSurface = ColorResources.Light.inverseSurface
)
色の定義
object ColorResources {
object Light {
val primary = Color(0xFF457DEF)
val onPrimary = Color(0xFFFFFFFF)
val primaryContainer = Color(0xFF7C99FC)
val onPrimaryContainer = Color(0xFFA2BEF7)
val secondary = Color(0xFF42C762)
val onSecondary = Color(0xFFFFFFFF)
val tertiary = Color(0xFFA2BEF7)
val background = Color(0xFFFFFFFF)
val onBackground = Color(0xFF282930)
val surface = Color(0xFFE8EAEE)
val onSurface = Color(0xFF666B75)
val surfaceVariant = Color(0xFFF5F5F5)
val onSurfaceVariant = Color(0xFF464F60)
val surfaceDim = Color(0xFF878D9A)
val error = Color(0xFFE13E3E)
val onError = Color(0xFFFFFFFF)
val outlineVariant = Color(0xFFE8EAEE)
val surfaceContainerHigh = Color(0xFFFBFBFB)
val surfaceContainer = Color(0xFFF5F7F9)
val surfaceContainerLow = Color(0xFFA4A8B3)
val inverseSurface = Color(0xFFABBEFE)
}
object Dark {
val primary = Color(0xFF3F64E5)
val onPrimary = Color(0xFFF0F0F2)
val primaryContainer = Color(0xFF2C4EA0)
val onPrimaryContainer = Color(0xFF203873)
val secondary = Color(0xFF32BA52)
val onSecondary = Color(0xFFF0F0F2)
val tertiary = Color(0xFF203873)
val background = Color(0xFF14151B)
val onBackground = Color(0xFFF0F0F2)
val surface = Color(0xFF35363B)
val onSurface = Color(0xFFA1A1A5)
val surfaceVariant = Color(0xFF24252B)
val onSurfaceVariant = Color(0xFFCECED4)
val surfaceDim = Color(0xFF7E7E86)
val error = Color(0xFFCC393A)
val onError = Color(0xFFF0F0F2)
val outlineVariant = Color(0xFF35363B)
val surfaceContainerHigh = Color(0xFF24252B)
val surfaceContainer = Color(0xFF1D1E24)
val surfaceContainerLow = Color(0xFF636369)
val inverseSurface = Color(0xFF304AA2)
}
}
-
ステップ4. shapesの実装
ここでは一般的によく使用される角丸の値を定義します
UI Designsでは特定のダイアログや要素の背景に角丸が必要な場合がよくあります
// in .~/commonMain/..
val shapes = Shapes(
extraLarge = RoundedCornerShape(30.dp),
large = RoundedCornerShape(24.dp),
medium = RoundedCornerShape(16.dp),
small = RoundedCornerShape(8.dp),
extraSmall = RoundedCornerShape(4.dp)
)
- 步驟5. 實作createTypography function來設定字體
實作function createTypography
並把前面做好的colorScheme丟進來
並使用TextStyle去設定各種字體的大小
可以根據UI/UX Designer的設計去決定
// in .~/commonMain/..
fun createTypography(colorScheme: ColorScheme) = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp,
color = colorScheme.onBackground
),
bodyMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 21.sp,
letterSpacing = 0.5.sp,
color = colorScheme.onBackground
),
bodySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 13.sp,
lineHeight = 20.sp,
letterSpacing = 0.5.sp,
color = colorScheme.onBackground
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
lineHeight = 30.sp,
letterSpacing = 0.sp,
color = colorScheme.onBackground
),
titleMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
lineHeight = 27.sp,
letterSpacing = 0.sp,
color = colorScheme.onBackground
),
titleSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.sp,
color = colorScheme.onBackground
),
labelLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 13.sp,
lineHeight = 20.sp,
letterSpacing = 0.sp,
color = colorScheme.surfaceDim
),
labelMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 18.sp,
letterSpacing = 0.sp,
color = colorScheme.surfaceDim
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 10.sp,
lineHeight = 13.sp,
letterSpacing = 0.sp,
color = colorScheme.surfaceDim
)
/* Other default text styles to override
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
- 步驟6. 實作設定status bar樣式
commonMain中加入 expect function setStatusBarStyle
因雙平台status bar不同
我們需要設定Android跟iOS平台
所以透過expect 實作function
// in .~/commonMain/StatusBarStyle.kt
@Composable
expect fun setStatusBarStyle(
backgroundColor: Color,
isDarkTheme: Boolean
)
實作 android actual 來設定Android Status Bar
這邊跟前面不同的是 這個要放在android實作資料夾下
// in .~/androidMain/StatusBarStyle.android.kt
@Composable
actual fun setStatusBarStyle(backgroundColor: Color, isDarkTheme: Boolean) {
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = backgroundColor.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
}
}
}
實作iOS actual 來設定iOS Status Bar
跟前面不同的是
這個要放在iOS實作資料夾下
// in .~/iosMain/StatusBarStyle.ios.kt
@Composable
actual fun setStatusBarStyle(backgroundColor: Color, isDarkTheme: Boolean) {
DisposableEffect(isDarkTheme) {
val statusBarStyle = if (isDarkTheme) {
UIStatusBarStyleLightContent
} else {
UIStatusBarStyleDarkContent
}
UIApplication.sharedApplication.setStatusBarStyle(statusBarStyle, animated = true)
onDispose { }
}
}
-
步驟7.
最後
在要使用Material 3 的地方
直接加入下面該主題:
@Preview
@Composable
fun PreviewSettingScreen() {
ElegantAccessComposeTheme {
SettingScreen(rememberNavController())
}
}
主題設定後
可以這樣使用
例如:
字體的bodySmall
是前面在寫createTypography 寫的
所以你只要去拿你預期中對應的變數即可
顏色也一樣
colorScheme是前面寫好的
你去選你要對應的顏色即可
字體設定
Text(
text = "Description: $it",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
顏色設定
Icon(
painterResource(Res.drawable.caret_right),
contentDescription = "Select",
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
以上 就能透過一次性的把主題色給設定好
以後要改也很方便只要改一個地方