Jetpack Compose material3 커스텀 테마 설정하기

jayden·2022년 3월 10일
2

안드로이드 작업을 하다보면, 항상 다크모드와 라이트 모드를 모두 지원하기는 여간 귀찮은 일이 아니다. 이 떄 머티리얼을 사용하여 컬러를 적용하면, 보다 쉽게 다크모드를 지원하는 앱을 만들 수 있다고 한다.

하지만 현실세계의 개발은 그리 녹록지 않다. 머티리얼에서 제공하는 롤과 토큰들은 디자이너의 니즈를 모두 충족시키기에는 턱 없이 부족한 토큰들이다.

그래서 몇몇 컴포넌트들은 별도로 컬러를 지정해주게 되는데, 이때마다 컬러를 코드에 입력하기엔 관리 포인트도 늘어나고 다크모드까지 제공하려면 여간 힘든일이 아니다.

따라서 이 글에선 필자가 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 함수에서밖에 되지 않는다..

필자가 적용하고싶은 코드는 머티리얼 처럼 기본 오브젝트를 가져오면 알아서 다크테마로 변경이 됐으면 좋겠다. 다음과 같이 말이다.

@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,
        ),
    )
}
profile
앱개발도 간간이 하는 서버 개발자 입니다.

0개의 댓글