일관성 있는 UI를 위한 Compose에서 theme관리하는 방법
data class MyColors(
val material: ColorScheme,
val tertiary: Color = material.primary,
val onPrimaryAlt: Color = material.onPrimary,
val success: Color = Color.Green,
val checked: Color = Color.White,
val unchecked: Color = Color.White,
val checkmark: Color = material.primary,
val disabledSecondary: Color = material.secondary.copy(alpha = 0.5f),
val textFiledBackground: Color = Color.LightGray,
val textFiledBackgroundVariant: Color = Color.DarkGray,
val launcherScreenBackground: Color = material.primary,
val progressItemColor: Color = Color.Black
) {
val primary: Color get() = material.primary
val primaryContainer: Color get() = material.primaryContainer
val secondary: Color get() = material.secondary
val secondaryContainer: Color get() = material.secondaryContainer
val background: Color get() = material.background
val surface: Color get() = material.surface
val error: Color get() = material.error
val onPrimary: Color get() = material.onPrimary
val onSecondary: Color get() = material.onSecondary
val onBackground: Color get() = material.onBackground
val onSurface: Color get() = material.onSurface
val onSurfaceVariant: Color get() = material.onSurfaceVariant
val onError: Color get() = material.onError
}
앱에 사용할 커스텀 컬러 data class 를 만들어준다.
colorScheme을 지정해주고 사용하는 메소드들을 정의 해준다.
이제 Light모드와 Dark모드를 구별해서 사용 할 수 있는 ColorSet를 만들어준다.
val Red400 = Color(0xFFFF5258)
val Red700 = Color(0xFFEC0000)
val Red800 = Color(0xFFAF0000)
...
sealed class ColorSet {
open lateinit var LightColors: MyColors
open lateinit var DarkColors: MyColors
data object Red: ColorSet() {
override var LightColors = MyColors(
material = lightColorScheme(
primary = Red700,
onPrimary = Red800,
secondary = Purple900,
onSecondary = Purple700,
surface = White,
onSurface = Black,
background = White,
onBackground = Black,
error = Red400
),
success = Green400,
disabledSecondary = Grey200,
textFiledBackground = Grey200
)
override var DarkColors = MyColors(
material = darkColorScheme (
primary = Purple900,
onPrimary = Red800,
secondary = Purple900,
onSecondary = Purple700,
surface = White,
onSurface = Black,
background = White,
onBackground = Black,
error = Red400,
)
)
}
}
ColorSet을 설정한뒤 Theme.kt의 AppTheme컴포저블에서 설정해준다.
private val LocalColors = staticCompositionLocalOf { ColorSet.Red.LightColors }
@Composable
fun MovieAppTheme(
myColors: ColorSet = ColorSet.Red,
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val colors =
if (darkTheme) myColors.DarkColors else myColors.LightColors
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colors.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
CompositionLocalProvider(LocalColors provides colors) {
MaterialTheme(
colorScheme = colors.material,
content = content,
)
}
}
여기서 CompositionLocalProvider를 사용해서 하위 컴포넌트로 필요한 데이터를 효율적으로 내려준다.
CompositionLocal을 사용하면 필요한 데이터만 상위에서 하위로 내려줌으로 불필요한 리컴포지션을 방지해준다.
MaterialTheme에선 color뿐만아니라 typography,shapes또한 설정이 가능하다.
Type.kt
private val spoqaHanSansNeo = FontFamily(
Font(R.font.spoqa_han_sans_neo_bold, FontWeight.Bold),
Font(R.font.spoqa_han_sans_neo_regular, FontWeight.Normal),
Font(R.font.spoqa_han_sans_neo_thin, FontWeight.Thin)
)
val Typography = Typography(
displayLarge = TextStyle(
fontFamily = spoqaHanSansNeo,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp
),
displayMedium = TextStyle(
fontFamily = spoqaHanSansNeo,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
lineHeight = 40.sp
),
...
theme에서 사용할 폰트들을 설정해줄 수 있다.
Text(
text = titleName,
modifier = Modifier.padding(Padding.large),
style = MaterialTheme.typography.headlineSmall
)
theme에 추가했다면 컴포저블의 style에서 타이포그래피를 설정해 스타일을 지정해 줄수 있다.
material3에선 style설정을 안할시 Text에선 bodyLarge를 기본으로 따라 가는것 같다.
기본 타이포 그래피에서 설정하는 값들이 부족하다, 좀더 세분화 해서 사용하고 싶다 할 수 있는데, 확장 프로퍼티를 통해 표현가능하다.
val Typography.displayLarge60: TextStyle
@Composable get() = displayLarge.copy(
fontSize = 60.sp
)
Text(
text = titleName,
modifier = Modifier.padding(Padding.large),
style = MaterialTheme.typography.displayLarge60
)
Typography의 설정을 변경하지 않고도 필요한 부분을 수정할 수 있어 관심사가 분리된다.
시스템을 유연하고 확장성있게 관리 할 수 있다.
결합도 또한 낮아지는데, Typography에 직접적인 영향을 끼치지 않기 때문에 결합도가 낮아 안정성이 늘어난다.
shape은 card등의 corner radius를 관리한다.
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(12.dp),
)
그냥 이거선언하고 테마에 넣으면 기본 radius가 조절된다.
padding같은경우 object를 이용해 관리할 수 있다.
object Padding {
val none = 0.dp
val xsmall = 2.dp
val small = 4.dp
val medium = 8.dp
val large = 12.dp
val xlarge = 16.dp
val extra = 24.dp
val xextra = 32.dp
}
직접적인 수 8.dp같은건 잘 안쓰인다고 한다.
그럼 modifier등에 설정하는 크기는 컴포저블마다 다를텐데 어떻게 할까?
private val CARD_WIDTH = 150.dp
private val CARD_HEIGHT = 200.dp
private val ICON_SIZE = 12.dp
@Composable
fun MovieItem() {
Column(
modifier = Modifier
.width(CARD_WIDTH)
.padding(Padding.large)
) {
Poster(
modifier = Modifier
.width(CARD_WIDTH)
)
각각 컴포저블을 구성하는 파일에 들어가 private하게 설정해준다.