컴포즈로 Material Theme를 사용하다보면, 테마 가짓수에 고개를 갸우뚱하곤 합니다.
(약 12가지 컬러를 지원한다.)팔레트 수가 적다보니 아래와 같은 이슈에 대응하기 어렵습니다.
이럴때 테마를 새로 만드는 방법도 고려해볼 수 있습니다.
왜 추천이 아닌 고려일까요?
Custom Theme를 만들었을때의 Trade Off는 바로 기본 컴포넌트에 영향을 미친다는 것입니다.
아래의 코드를 한번 보실까요?
Compose에서 제공되는 컴포넌트 중에는 MaerialTheme가 하드코딩되어있는 컴포넌트들이 많습니다.
결국 요구사항에 맞추기 위해서는 Component 내부를 복사하여 입맛에 맞게 수정하는일이 빈번해지게 됩니다.
색깔을 지원하기 위해선 색깔을 파라미터로 가지는 Class가 필요합니다.
class PollPollColors(
//색깔을 파라미터로 입력한다.
primary_500: Color,
primary_300: Color,
primary_100: Color,
primary_050: Color,
secondary_500: Color,
secondary_300: Color,
secondary_050: Color,
gray_050: Color,
gray_100: Color,
gray_200: Color,
gray_300: Color,
gray_400: Color,
gray_500: Color,
gray_700: Color,
gray_900: Color,
isLight: Boolean
)
Color를 그대로 사용한다면 상관이 없지만, 다크모드로 변경되었을때 Recomposition이 일어나야합니다.
그렇기에 받은 파라미터가 안에서 State로 Wrapping 되어야 합니다.
class PollPollColors(
//색깔을 파라미터로 입력한다.
primary_500: Color,
primary_300: Color,
primary_100: Color,
primary_050: Color,
secondary_500: Color,
secondary_300: Color,
secondary_050: Color,
gray_050: Color,
gray_100: Color,
gray_200: Color,
gray_300: Color,
gray_400: Color,
gray_500: Color,
gray_700: Color,
gray_900: Color,
isLight: Boolean
) {
var primary_500 by mutableStateOf(primary_500)
private set
var primary_300 by mutableStateOf(primary_300)
private set
var primary_100 by mutableStateOf(primary_100)
private set
var primary_050 by mutableStateOf(primary_050)
private set
var secondary_500 by mutableStateOf(secondary_500)
private set
var secondary_300 by mutableStateOf(secondary_300)
private set
var secondary_050 by mutableStateOf(secondary_050)
private set
var gray_050 by mutableStateOf(gray_050)
private set
var gray_100 by mutableStateOf(gray_100)
private set
var gray_200 by mutableStateOf(gray_200)
private set
var gray_300 by mutableStateOf(gray_300)
private set
var gray_400 by mutableStateOf(gray_400)
private set
var gray_500 by mutableStateOf(gray_500)
private set
var gray_700 by mutableStateOf(gray_700)
private set
var gray_900 by mutableStateOf(gray_900)
private set
var isLight by mutableStateOf(isLight)
private set
}
색깔은 바뀔때마다 recomposition
이 발생합니다.
만약 새로운 객체로 바꾸게 된다면, 모든 컴포저블이 재구성된다는 얘기인데요.
변경을 최소화하기 위해 updateColorsFrom
을 사용하여 필요한 부분만 변경하도록하여 recompostion
을 최소화 시킵니다.
/**지정된 ColorScheme 의 내부 값을 other 의 값으로 업데이트합니다.
이렇게 하면 LocalColorScheme 의 값을 사용하는 모든 컴포저블을 재구성하지 않고도 ColorScheme 의 하위 집합을 효율적으로 업데이트할 수 있습니다.
ColorScheme 은 매우 광범위하고 계층 구조의 많은 고가의 컴포저블에서 사용되기 때문에 LocalColorScheme에 새 값을 제공하면 LocalColorScheme 을 사용하는 모든 LocalColorScheme 이 재구성됩니다.
이는 테마의 한 색상에 애니메이션을 적용하는 것과 같은 경우 엄청나게 비용이 많이 듭니다.
대신 ColorScheme 은 mutableStateOf 에 의해 내부적으로 지원되며 이 함수는 other 의 값과 일치하도록 this 의 내부 상태를 변경합니다.
즉, 모든 변경사항은 this 의 내부 상태를 변경하고 변경된 특정 값을 읽는 컴포저블만 재구성하도록 합니다.
*/
fun copy(
primary_500: Color = this.primary_500,
primary_300: Color = this.primary_300,
primary_100: Color = this.primary_100,
primary_050: Color = this.primary_050,
secondary_500: Color = this.secondary_500,
secondary_300: Color = this.secondary_300,
secondary_050: Color = this.secondary_050,
gray_050: Color = this.gray_050,
gray_100: Color = this.gray_100,
gray_200: Color = this.gray_200,
gray_300: Color = this.gray_300,
gray_400: Color = this.gray_400,
gray_500: Color = this.gray_500,
gray_700: Color = this.gray_700,
gray_900: Color = this.gray_900,
isLight: Boolean = this.isLight
) = PollPollColors(
primary_500 = primary_500,
primary_300 = primary_300,
primary_100 = primary_100,
primary_050 = primary_050,
secondary_500 = secondary_500,
secondary_300 = secondary_300,
secondary_050 = secondary_050,
gray_050 = gray_050,
gray_100 = gray_100,
gray_200 = gray_200,
gray_300 = gray_300,
gray_400 = gray_400,
gray_500 = gray_500,
gray_700 = gray_700,
gray_900 = gray_900,
isLight = false
)
fun updateColorsFrom(other: PollPollColors) {
primary_500 = other.primary_500
primary_300 = other.primary_300
primary_100 = other.primary_100
primary_050 = other.primary_100
secondary_500 = other.secondary_500
secondary_300 = other.secondary_300
secondary_050 = other.secondary_050
gray_050 = other.gray_050
gray_100 = other.gray_100
gray_200 = other.gray_200
gray_300 = other.gray_300
gray_400 = other.gray_400
gray_500 = other.gray_500
gray_700 = other.gray_700
gray_900 = other.gray_900
isLight = false
}
Typography의 값은 Color값처럼 변하지 않습니다.
recomposition이 일어나지 않아도 되기때문에 State()로 감쌀 이유가 없습니다.
val PollPollFamily = FontFamily(
Font(R.font.pretendard_regular, FontWeight.Normal),
Font(R.font.pretendard_semibold, FontWeight.SemiBold)
)
//폰트 패밀리가 폰트웨이트에 따라 알아서 선택이 된다.
data class CustomTypography(
val heading01: TextStyle = TextStyle(
fontFamily = PollPollFamily,
fontWeight = FontWeight.SemiBold,
lineHeight = 38.sp,
fontSize = 28.sp
),
val heading02: TextStyle = TextStyle(
fontFamily = PollPollFamily,
fontWeight = FontWeight.SemiBold,
lineHeight = 32.sp,
fontSize = 24.sp
),
Space라던지, Shapes를 정의할 수도 있습니다.
TypoGraphy와 같이 변경되지 않기에 dataclass
를 정의 해서 사용하기도하고,
data class CustomSpaces(
val small: Dp = 4.dp,
val medium: Dp = 8.dp,
val large: Dp = 16.dp,
val extraLarge: Dp = 40.dp
)
기존 MaterialTheme에 있는것을 활용하고 싶다면, 객체를 재활용해도 좋습니다.
val Shapes = Shapes(
small = RoundedCornerShape(Dimens.SpacingXXS),
medium = RoundedCornerShape(Dimens.SpacingXXS),
large = RoundedCornerShape(Dimens.SpacingXS)
)
지금까지 만든 모든 테마의 클래스들과 객체들은 CompositionLocalProvider
를 통해 제공됩니다.
@Composable
fun PollPollTheme(
spaces: CustomSpaces = PollPollTheme.spaces,
typography: CustomTypography = PollPollTheme.typography,
colors: PollPollColors = PollPollTheme.colors,
darkColors: PollPollColors? = null,
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val currentColor = remember { if (darkColors != null && darkTheme) darkColors else colors }
//변경에 recomposition이 최소화되기위해 부분적인 업데이트가 필요하다.
val rememberedColors = remember { currentColor.copy() }.apply { updateColorsFrom(currentColor) }
CompositionLocalProvider(
LocalColors provides rememberedColors,
LocalSpaces provides spaces,
LocalTypography provides typography
) {
//Typography의 TextStyle은 조금 다르게 선언된다.
ProvideTextStyle(typography.heading05, content = content)
}
}
val LocalSpaces = staticCompositionLocalOf { CustomSpaces() }
val LocalColors = staticCompositionLocalOf { lightColors() }
val LocalTypography = staticCompositionLocalOf { CustomTypography() }
CompositionLocalProvider로 주입된 애들은 LocalSpaces.current
나 LocalColors.current
등을 통해 값을 가져올 수 있습니다.
이를 좀더 쉽게 액세스 할수 있도록 object
로 감쌉니다.
참고로 @ReadOnlyComposable
컴포저블함수를 읽기에만 최적화되도록 만들어주는 어노테이션입니다.
object PollPollTheme {
val colors: PollPollColors
@Composable
@ReadOnlyComposable
get() = LocalColors.current
val typography: CustomTypography
@Composable
@ReadOnlyComposable
get() = LocalTypography.current
val spaces: CustomSpaces
@Composable
@ReadOnlyComposable
get() = LocalSpaces.current
}
꽤나 큰 프로젝트에는 어쩔수 없이 Custom Theme를 만들어야 할것 같습니다.
감사합니다! <3