컴포지션 트리를 따라 값을 암묵적으로 전달하는 통로.
val LocalSpacing = staticCompositionLocalOf { Spacing() }
CompositionLocalProvider(LocalSpacing provides Spacing(large = 24.dp)) {
MyScreen() // 트리 어디서든 LocalSpacing.current 로 읽는다
}
@Composable fun DeepChild() {
Box(Modifier.padding(LocalSpacing.current.large))
}
이미 쓰고 있는 것들
- LocalContext — 안드로이드 Context
- LocalConfiguration — 화면 크기 · 방향 · 다크모드
- LocalDensity — dp ↔ px 변환
- LocalLifecycleOwner — 생명주기
- LocalFocusManager — 포커스 제어
- MaterialTheme — 내부적으로 CompositionLocal 로 구현돼 있다
두 가지 생성 함수
-
compositionLocalOf — 값이 자주 바뀐다
- 읽는 곳만 무효화한다 (추적 비용이 있다)
-
staticCompositionLocalOf — 값이 거의 안 바뀐다
- 바뀌면 Provider 아래 전체를 다시 그린다
- 대신 읽기가 싸다
-
테마 · 간격처럼 사실상 고정인 값 → static
언제 쓰지 않나
❌ ViewModel 이나 Repository 를 CompositionLocal 로 내려보낸다
- 의존성이 시그니처에 안 보인다
- 미리보기와 테스트에서 Provider 를 빠뜨려 크래시한다
- 어떤 값을 받는지 호출부가 모른다
✅ 파라미터로 명시적으로 넘긴다
기준: "트리 대부분이 쓰고, 중간 계층이 신경 쓸 필요가 없는 횡단 관심사인가?"
- 테마 · 간격 · 밀도 · 접근성 설정 → 예
- 화면 데이터 · 콜백 → 아니오
기본값이 없으면 크래시한다
val LocalUser = staticCompositionLocalOf<User> {
error("LocalUser 가 제공되지 않았습니다")
}
Provider 를 감싸지 않고 읽으면 이 예외가 난다 @Preview 에서 특히 자주 터진다 — 미리보기용 Provider 를 함께 만들어 둔다
View interop
// Compose 안에 View 를 넣는다
AndroidView(
factory = { ctx -> MapView(ctx) },
update = { it.setCenter(lat, lng) } // 리컴포지션마다 불린다
)
// View 안에 Compose 를 넣는다
ComposeView(context).apply {
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) // ★ 누수 방지
setContent { MyComposable() }
}
면접 함정
- ❌ "CompositionLocal은 전역 변수와 같다" → 트리 범위다. 같은 Local이 위치에 따라 다른 값일 수 있다.
- ❌
AndroidView의factory에서 상태를 읽는다 →factory는 한 번만 불린다. 갱신은update에서 한다.