색 · 타이포 · 셰이프의 단일 출처.
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) DarkColors else LightColors,
typography = AppTypography,
shapes = AppShapes
) { content() }
// 사용
Text("제목", style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface)
색을 "역할"로 쓴다
❌ Color(0xFF1A1A1A) 를 직접 쓴다
- 다크모드에서 검은 배경에 검은 글씨가 된다
- 색을 바꾸려면 200곳을 찾아야 한다
✅ colorScheme.onSurface 를 쓴다
- 라이트/다크 각각의 스킴이 알맞은 값을 준다
- 다크모드가 공짜가 된다
짝을 이루는 규칙
primary ↔ onPrimary (primary 배경 위의 내용 색)
surface ↔ onSurface
error ↔ onError
"on" 이 붙은 색은 그 배경 위에서 대비가 보장된다 — 접근성이 설계에 들어있다
동적 색상
val colors = when {
Build.VERSION.SDK_INT >= 31 && dynamicColor ->
if (dark) dynamicDarkColorScheme(ctx) else dynamicLightColorScheme(ctx)
dark -> DarkColors
else -> LightColors
}
Android 12+ 에서 사용자의 배경화면에서 뽑은 색을 쓴다
- 시스템과 어우러진다
대가: 브랜드 색이 사라진다
- 브랜드가 중요한 앱은 끄거나 일부만 적용한다
다크모드는 색만이 아니다
- 이미지의 밝기 (어두운 배경에 흰 로고)
- 그림자 대신 표면 톤 (다크에서는 elevation 이 색으로 표현된다)
- 상태바/내비바 아이콘 색 (WindowInsetsControllerCompat)
isSystemInDarkTheme() 은 시스템 설정만 본다 앱 내 토글을 두려면 값을 DataStore 에 저장해 직접 관리한다
자체 디자인 시스템으로 확장
// Material 에 없는 토큰을 CompositionLocal 로 추가한다
val LocalSpacing = staticCompositionLocalOf { Spacing() }
data class Spacing(val small: Dp = 4.dp, val medium: Dp = 8.dp, val large: Dp = 16.dp)
CompositionLocalProvider(LocalSpacing provides Spacing()) { MaterialTheme { content() } }
면접 함정
- ❌ "다크모드는 색 반전" → 대비·이미지·elevation 표현까지 함께 설계해야 한다.
- ❌ "테마를 안 쓰고 상수 파일에 색을 모으면 같다" → 컴포지션 위치에 따라 다른 테마를 줄 수 없고, 미리보기에서 테마 전환도 안 된다.
미리보기에서 두 테마를 함께 본다
@Preview(name = "라이트")
@Preview(name = "다크", uiMode = Configuration.UI_MODE_NIGHT_YES)
annotation class ThemePreviews
@ThemePreviews
@Composable fun HomePreview() = AppTheme { HomeScreen(state = sampleState) }
elevation이 다크에서 색으로 표현된다
Material 3 에서 표면 톤(surface tone) 이 계층을 표현한다
라이트 그림자로 떠 있음을 표현
다크 그림자가 안 보이므로 표면을 더 밝게 해 표현
Surface(tonalElevation = 3.dp) 를 쓰면 두 모드에서 알아서 처리된다 색을 직접 지정하면 다크에서 계층이 평평해진다
시스템 바 색을 맞춘다
val view = LocalView.current
SideEffect {
val window = (view.context as Activity).window
WindowCompat.getInsetsController(window, view)
.isAppearanceLightStatusBars = !darkTheme
}
안 맞추면 라이트 배경에 흰 상태바 아이콘이 겹쳐 안 보인다 (Android 15+ 는 edge-to-edge 가 기본이라 더 자주 마주친다)