CompositionLocal
은 암시적으로 컴포지션을 통해 데이터를 전달하는 도구입니다. 이 페이지에서는 CompositionLocal
의 자세한 내용과 자체 CompositionLocal
을 만드는 방법, CompositionLocal
이 사용 사례에 적합한 솔루션인지 확인하는 방법을 알아봅니다.
CompositionLocal
소개
일반적으로 Compose에서 데이터는 각 구성 가능한 함수의 매개변수로 UI 트리를 통해 아래로 흐릅니다. 따라서 컴포저블의 종속 항목이 명시적으로 됩니다. 그러나 이 방법은 색상이나 유형 스타일과 같이 매우 자주 널리 사용되는 데이터의 경우에는 번거로울 수 있습니다. 아래 예를 참고하세요.
@Composable fun MyApp() { // Theme information tends to be defined near the root of the application val colors = colors() } // Some composable deep in the hierarchy @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, color = colors.onPrimary // ← need to access colors here ) }
색상을 대부분의 컴포저블에 명시적 매개변수 종속 항목으로 전달할 필요가 없도록 지원하기 위해 Compose는 CompositionLocal
을 제공합니다. 이를 통해 UI 트리를 통해 데이터 흐름이 발생하는 암시적 방법으로 사용할 수 있는 트리 범위의 명명된 객체를 만들 수 있습니다.
CompositionLocal
요소는 일반적으로 UI 트리의 특정 노드의 값과 함께 제공됩니다. 이 값은 구성 가능한 함수에서 CompositionLocal
을 매개변수로 선언하지 않아도, 구성 가능한 하위 요소에 사용할 수 있습니다.
CompositionLocal
은 Material 테마에서 내부적으로 사용하는 것입니다.
MaterialTheme
은 나중에 컴포지션의 하위 부분에서 가져올 수 있는 세 개의 CompositionLocal
인스턴스(colorScheme
, typography
, shapes
)를 제공하는 객체입니다.
이러한 인스턴스는 구체적으로 LocalColorScheme
, LocalShapes
, LocalTypography
속성으로, MaterialTheme
colorScheme
, shapes
, typography
속성을 통해 액세스할 수 있습니다.
@Composable fun MyApp() { // Provides a Theme whose values are propagated down its `content` MaterialTheme { // New values for colorScheme, typography, and shapes are available // in MaterialTheme's content lambda. // ... content here ... } } // Some composable deep in the hierarchy of MaterialTheme @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, // `primary` is obtained from MaterialTheme's // LocalColors CompositionLocal color = MaterialTheme.colorScheme.primary ) }
CompositionLocal
인스턴스는 컴포지션의 일부로 범위가 지정되므로 트리의 여러 수준에서 다양한 값을 제공할 수 있습니다. CompositionLocal
의 current
값은 컴포지션에서 범위가 지정된 부분의 상위 요소가 제공한 가장 가까운 값에 대응합니다.
새 값을 CompositionLocal
에 제공하려면 CompositionLocalProvider
와 CompositionLocal
키를 value
에 연결하는 provides
중위 함수를 사용합니다. CompositionLocalProvider
의 content
람다는 CompositionLocal
의 current
속성에 액세스할 때 제공된 값을 가져옵니다. 새 값이 제공되면 Compose는 CompositionLocal
을 읽는 컴포지션의 부분을 재구성합니다.
예를 들어 LocalContentColor
CompositionLocal
에는 텍스트와 아이콘에 사용되는 기본 콘텐츠 색상이 포함되어 있어 현재 배경색과 대비되도록 합니다. 다음 예에서 CompositionLocalProvider
는 컴포지션의 여러 부분에 다양한 값을 제공하는 데 사용됩니다.
@Composable fun CompositionLocalExample() { MaterialTheme { // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default // This is to automatically make text and other content contrast to the background // correctly. Surface { Column { Text("Uses Surface's provided content color") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { Text("Primary color provided by LocalContentColor") Text("This Text also uses primary as textColor") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { DescendantExample() } } } } } } @Composable fun DescendantExample() { // CompositionLocalProviders also work across composable functions Text("This Text uses the error color now") }
그림 1. CompositionLocalExample
컴포저블의 미리보기
마지막 예에서 CompositionLocal
인스턴스는 머티리얼 컴포저블에서 내부적으로 사용되었습니다. CompositionLocal
의 현재 값에 액세스하려면 current
속성을 사용합니다. 다음 예에서는 Android 앱에서 흔히 사용되는 LocalContext
CompositionLocal
의 현재 Context
값을 사용하여 텍스트의 형식을 지정합니다.
@Composable fun FruitText(fruitSize: Int) { // Get `resources` from the current value of LocalContext val resources = LocalContext.current.resources val fruitText = remember(resources, fruitSize) { resources.getQuantityString(R.plurals.fruit_title, fruitSize) } Text(text = fruitText) }
나만의 CompositionLocal
만들기
CompositionLocal
은 암시적으로 컴포지션을 통해 데이터를 전달하는 도구입니다.
CompositionLocal
사용을 위한 또 다른 주요 신호는 매개변수가 크로스 커팅이고 구현의 중간 레이어가 그 존재를 인식해서는 안 되는 경우입니다. 이러한 중간 레이어가 인식하도록 하면 컴포저블의 유틸리티가 제한될 수 있기 때문입니다. 예를 들어 Android 권한 쿼리는 내부적으로 CompositionLocal
에서 제공됩니다. 미디어 선택 도구 컴포저블은 API를 변경하지 않고 미디어 선택 도구 호출자가 환경에서 사용하는 이러한 추가 컨텍스트를 인식하도록 요구하지 않고도 기기에서 권한으로 보호되는 콘텐츠에 액세스하는 새로운 기능을 추가할 수 있습니다.
그러나 CompositionLocal
이 항상 최선의 솔루션은 아닙니다. CompositionLocal
을 과도하게 사용하지 않는 것이 좋습니다. 다음과 같은 단점이 있기 때문입니다.
CompositionLocal
은 컴포저블의 동작을 추론하기 어렵게 합니다. 암시적 종속 항목을 만들 때 이를 사용하는 컴포저블의 호출자는 모든 CompositionLocal
의 값이 충족되는지 확인해야 합니다.
또한 이 종속 항목은 컴포지션의 모든 부분에서 변경될 수 있으므로 종속 항목에 관한 명확한 정보 소스가 없을 수도 있습니다. 따라서 문제가 발생할 때 앱을 디버깅하는 것이 더 어려울 수 있습니다. 컴포지션을 탐색하여 current
값이 제공된 위치를 확인해야 하기 때문입니다. IDE의 Find usages나 Compose 레이아웃 검사기와 같은 도구에서는 이 문제를 완화할 정보를 충분히 제공합니다.
CompositionLocal
사용 여부 결정
특정 조건에서는 사용 사례에 CompositionLocal
이 적합한 솔루션이 될 수 있습니다.
CompositionLocal
에는 적절한 기본값이 있어야 합니다. 기본값이 없으면 개발자가 CompositionLocal
의 값이 제공되지 않는 매우 곤란한 상황에 처할 수 있다는 것을 확실하게 해야 합니다.
기본값을 제공하지 않으면 테스트를 만들거나 CompositionLocal
을 사용하는 컴포저블을 미리 볼 때 항상 명시적으로 제공되도록 기본값을 요구해야 하는 문제와 불만이 발생할 수 있습니다.
트리 범위 또는 하위 계층 구조 범위로 간주되지 않는 개념에는 CompositionLocal
을 사용하지 않습니다. CompositionLocal
은 잠재적으로 일부 하위 요소가 아닌 모든 하위 요소에서 사용할 수 있을 때 적합합니다.
사용 사례가 이러한 요구사항을 충족하지 않는다면 CompositionLocal
을 만들기 전에 고려할 대안 섹션을 확인하세요.
좋지 않은 방법의 예는 특정 화면의 ViewModel
을 보유하는 CompositionLocal
을 만들어 이 화면의 모든 컴포저블이 일부 로직을 실행하는 ViewModel
을 참조할 수 있도록 하는 것입니다. 이 방법이 좋지 않은 이유는 특정 UI 트리 아래의 모든 컴포저블이 ViewModel
에 관해 알 필요는 없기 때문입니다. 상태는 아래로 흐르고 이벤트는 위로 흐르는 패턴에 따라 필요한 정보만 컴포저블에 전달하는 것이 좋습니다. 이 방법을 통해 컴포저블을 재사용하고 테스트하기가 더 쉬워집니다.
CompositionLocal
만들기
CompositionLocal
을 만드는 두 가지 API가 있습니다.
compositionLocalOf
: 재구성 중에 제공된 값을 변경하면current
값을 읽는 콘텐츠만 무효화됩니다.staticCompositionLocalOf
:compositionLocalOf
와 달리staticCompositionLocalOf
읽기는 Compose에서 추적하지 않습니다. 값을 변경하면 컴포지션에서current
값을 읽는 위치만이 아니라CompositionLocal
이 제공된content
람다 전체가 재구성됩니다.
CompositionLocal
에 제공된 값이 변경될 가능성이 거의 없거나 변경되지 않는다면 staticCompositionLocalOf
를 사용하여 성능 이점을 얻으세요.
예를 들어 앱의 디자인 시스템은 컴포저블이 UI 구성요소의 그림자를 사용하여 높아지는 방식으로 독단적일 수 있습니다. 앱의 다양한 고도는 UI 트리 전체에 전파되어야 하므로 CompositionLocal
을 사용합니다. CompositionLocal
값은 시스템 테마에 기반하여 조건부로 파생되므로 compositionLocalOf
API를 사용합니다.
// LocalElevations.kt file data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp) // Define a CompositionLocal global object with a default // This instance can be accessed by all composables in the app val LocalElevations = compositionLocalOf { Elevations() }
CompositionLocal
에 값 제공
CompositionLocalProvider
컴포저블은 주어진 계층 구조의 CompositionLocal
인스턴스에 값을 바인딩합니다. CompositionLocal
에 새 값을 제공하려면 다음과 같이 CompositionLocal
키를 value
에 연결하는 provides
중위 함수를 사용하세요.
// MyActivity.kt file class MyActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { // Calculate elevations based on the system theme val elevations = if (isSystemInDarkTheme()) { Elevations(card = 1.dp, default = 1.dp) } else { Elevations(card = 0.dp, default = 0.dp) } // Bind elevation as the value for LocalElevations CompositionLocalProvider(LocalElevations provides elevations) { // ... Content goes here ... // This part of Composition will see the `elevations` instance // when accessing LocalElevations.current } } } }
CompositionLocal
소비
CompositionLocal.current
는 CompositionLocal
에 값을 제공하는 가장 가까운 CompositionLocalProvider
에서 제공한 값을 반환합니다.
@Composable fun SomeComposable() { // Access the globally defined LocalElevations variable to get the // current Elevations in this part of the Composition MyCard(elevation = LocalElevations.current.card) { // Content } }
고려할 대안
일부 사용 사례에서는 CompositionLocal
이 지나친 솔루션일 수 있습니다. 사용 사례가 CompositionLocal 사용 여부 결정 섹션에 지정된 기준을 충족하지 않으면 다른 솔루션이 사용 사례에 더 적합할 수 있습니다.
명시적 매개변수 전달
컴포저블의 종속 항목에 관해 명시적인 것이 좋습니다. 컴포저블에는 필요한 것만 전달하는 것이 좋습니다. 컴포저블의 분리와 재사용을 촉진하려면 각 컴포저블이 가능한 가장 적은 정보를 보유해야 합니다.
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel.data) } // Don't pass the whole object! Just what the descendant needs. // Also, don't pass the ViewModel as an implicit dependency using // a CompositionLocal. @Composable fun MyDescendant(myViewModel: MyViewModel) { /* ... */ } // Pass only what the descendant needs @Composable fun MyDescendant(data: DataToDisplay) { // Display data }
컨트롤 역전
불필요한 종속 항목을 컴포저블에 전달하지 않는 또 다른 방법은 컨트롤 역전을 사용하는 것입니다. 하위 요소가 일부 로직을 실행하는 종속 항목을 가져오지 않고 대신 상위 요소가 실행합니다.
다음 예는 하위 요소가 일부 데이터 로드 요청을 트리거해야 하는 경우를 보여줍니다.
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
경우에 따라 MyDescendant
에는 많은 책임이 있을 수 있습니다. MyViewModel
을 종속 항목으로 전달하면 두 컴포저블이 함께 결합되기 때문에 MyDescendant
의 재사용성도 줄어듭니다. 종속 항목을 하위 요소에 전달하지 않고 상위 요소가 로직 실행을 담당하도록 하는 제어 역전 원칙을 사용하는 대안을 고려해보세요.
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
이 접근 방식은 일부 사용 사례에 더 적합할 수 있습니다. 하위 요소를 직계 상위 요소에서 분리하기 때문입니다. 상위 컴포저블은 더 유연한 하위 수준 컴포저블을 보유하기 위해 더 복잡해지는 경향이 있습니다.
마찬가지로 @Composable
콘텐츠 람다를 같은 방식으로 사용하여 동일한 이점을 얻을 수 있습니다.
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- Compose 내 테마 분석
- Compose에서 뷰 사용
- Jetpack Compose용 Kotlin