안드로이드 작업을 하다보면, 항상 다크모드와 라이트 모드를 모두 지원하기는 여간 귀찮은 일이 아니다. 이 떄 머티리얼을 사용하여 컬러를 적용하면, 보다 쉽게 다크모드를 지원하는 앱을 만들 수 있다고 한다.
하지만 현실세계의 개발은 그리 녹록지 않다. 머티리얼에서 제공하는 롤과 토큰들은 디자이너의 니즈를 모두 충족시키기에는 턱 없이 부족한 토큰들이다.
그래서 몇몇 컴포넌트들은 별도로 컬러를 지정해주게 되는데, 이때마다 컬러를 코드에 입력하기엔 관리 포인트도 늘어나고 다크모드까지 제공하려면 여간 힘든일이 아니다.
따라서 이 글에선 필자가 Jetpack-compose에서 커스텀으로 컬러를 사용하는 방법을 소개하고자 한다.
다음은 필자가 (정상적으로?) 머티리얼3 디자인을 사용하는 방법이다.
우선 팔레트, 토큰, 롤, 테마를 정의해준다.
// 팔레트
val primary_100 = Color(0xffffffff)
val primary_99 = Color(0xfff2fff4)
val primary_95 = Color(0xffbeffd6)
val primary_90 = Color(0xff91f7ba)
val primary_80 = Color(0xff75dba0)
val primary_70 = Color(0xff59be86)
val primary_60 = Color(0xff3ba36d)
val primary_50 = Color(0xff158855)
val primary_40 = Color(0xff006d41)
// .... 기타 코드들은 너무 많아 생략...
// 토큰
val md_theme_light_primary = primary_40
val md_theme_dark_primary = primary_80
// 롤-토큰
private val LightThemeColors = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
)
private val DarkThemeColors = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
)
// 앱 테마 설정
@Composable
fun AppTheme (content: @Composable () -> Unit) {
val isDynamicColor = true
val isDarkTheme = isSystemInDarkTheme()
val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
dynamicColor && isDarkTheme -> {
dynamicDarkColorScheme(LocalContext.current)
}
dynamicColor && !isDarkTheme -> {
dynamicLightColorScheme(LocalContext.current)
}
isDarkTheme -> DarkThemeColors
else -> LightThemeColors
}
MaterialTheme (
colorScheme = colorScheme,
typography = typography,
content = {
Surface(content = content)
},
)
}
다음으로 베이스 컴포넌트들을 만들어둔다.
@Composable
fun MyText(label: String) {
Text (
style = MaterialTheme.typography.labelSmall.copy(
color= MaterialTheme.colorScheme.primary
),
text = label
)
}
위와같이 머티리얼과 테마와 베이스 컴포넌트를 잡아두고 원하는 곳에서 아래처럼 사용하면, 디자이너와 약속된 디자인 가이드에다 따라 나름 편하게 개발을할 수 있게된다. (다크모드 걱정없이)
Column {
MyText("아이디")
}
이렇게만 만들어두고 끝나면 행복 코딩을 할거라 기대했으나, 한가지 문제가 생긴다. 메인UI에서 텍스트는 0xFF333333(lightmode) 0xFF999999(darkmode)을 사용해야한다는 것.. 그 말은 머티리얼3에서 제공하는 기본 롤 외에 다른 컬러를 사용할 일이 생긴 것이다.
급하게 다음과 같이 만들어본다.
@Composable
fun MyTextForMainUI(label: String) {
Text (
style = MaterialTheme.typography.labelSmall.copy(
color= if(isSystemInDarkTheme()) Color(0xFF999999) else Color(0xFF333333)
),
text = label
)
}
이러면 간단하게 정리가 된거같으나 위 코드에는 몇 가지 문제점이 있다.
1. 매직넘버로 코드가 박힌 점
2. MyTextForMainUI함수에 다크모드에대한 종속성이 생긴 점
필자가 적용하고싶은 코드는 머티리얼 처럼 기본 오브젝트를 가져오면 알아서 다크테마로 변경이 됐으면 좋겠다. 다음과 같이 말이다.
@Composable
fun MyPlaceholder(label: String) {
Text(
text = placeholder,
style = CustomMaterialTheme.typography.placeholderLarge.copy(
color= CustomMaterialTheme.colorScheme.mySchemePrimary,
),
)
}
위 두가지 문제를를 필자는 머티리얼3의 소스코드를 봐가며 다음과 같이 해결했다.
우선 헥스부터 지워보자, 커스텀 컬러 팔레트를 만든다.
val custom_99 = Color(0xff999999)
val custom_80 = Color(0xff888888)
val custom_30 = Color(0xff333333)
// ...
그리고나서 커스텀 컬러 스킴을 만들었다.
object CustomColorLightTokens {
val mySchemePrimary = custom_30
val mySchemeSecondary = custom_30
}
object CustomColorDarkTokens {
val mySchemePrimary = custom_99
val mySchemeSecondary = custom_80
}
class CustomColorScheme(
val mySchemePrimary: Color,
val mySchemeSecondary: Color,
)
fun customLightColorScheme(
mySchemePrimary: Color = CustomColorLightTokens.typoPrimary,
mySchemeSecondary: Color = CustomColorLightTokens.typoSecondary,
) : CustomColorScheme=
CustomColorScheme (
mySchemePrimary,
mySchemeSecondary,
)
fun customDarkColorScheme(
mySchemePrimary: Color = CustomColorDarkTokens.typoPrimary,
mySchemeSecondary: Color = CustomColorDarkTokens.typoSecondary,
) : CustomColorScheme=
CustomColorScheme (
mySchemePrimary,
mySchemeSecondary,
)
이제 커스텀 스킴이 생겼다! 이를 이제 소스에서 사용하기 위해 상수로 만들어보자.
private val LightCustomThemeColors = customLightColorScheme()
private val DarkCustomThemeColors = customDarkColorScheme(
mySchemeSecondary = /*새로운 컬러*/,
)
자 이제 매직넘버로 코드에서 작성하던 문제가 해결되고, 2번 문제인 다크모드에 대한 종속성을 제거하고자한다.
우선 오브젝트를 하나 만들고 LocalProvider로 등록할 수 있도록 설정해보자
object CustomMaterialTheme {
val colorScheme: CustomColorScheme
@Composable
@ReadOnlyComposable
get() = CustomLocalColorScheme.current
// 타이포도 위에서 만들었던과 같은 방식으로 만들어주면 된다!
val typography: CustomTypography
@Composable
@ReadOnlyComposable
get() = CustomLocalTypography.current
}
internal val CustomLocalColorScheme = staticCompositionLocalOf { customLightColorScheme() }
internal val CustomLocalTypography = staticCompositionLocalOf { CustomTypography() }
이제 다 끝났다. 앱 테마 설정에서 local provier와 함께 설정해준다.
// 앱 테마 설정
@Composable
fun AppTheme (content: @Composable () -> Unit) {
val isDynamicColor = true
val isDarkTheme = isSystemInDarkTheme()
val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
dynamicColor && isDarkTheme -> {
dynamicDarkColorScheme(LocalContext.current)
}
dynamicColor && !isDarkTheme -> {
dynamicLightColorScheme(LocalContext.current)
}
isDarkTheme -> DarkThemeColors
else -> LightThemeColors
}
val customColorScheme = when {
isDarkTheme -> DarkCustomThemeColors
else -> LightCustomThemeColors
}
MaterialTheme (
colorScheme = colorScheme,
typography = typography,
content = {
CompositionLocalProvider(
CustomLocalColorScheme provides customColorScheme,
CustomLocalTypography provides customTypography
) {
Surface(content = content)
}
},
)
}
이렇게 커스텀 테마를 만들고나면 이제 앞에서 소개한 것과 같이 다음처럼 쓸 수 있게된다! 야호!
@Composable
fun MyPlaceholder(label: String) {
Text(
text = placeholder,
style = CustomMaterialTheme.typography.placeholderLarge.copy(
color= CustomMaterialTheme.colorScheme.mySchemePrimary,
),
)
}