시배's Android

Compose Docs | Locally scoped data with CompositionLocal 본문

Android/Compose Docs

Compose Docs | Locally scoped data with CompositionLocal

si8ae 2023. 8. 13. 19:37
 

CompositionLocal을 사용한 로컬 범위 지정 데이터  |  Jetpack Compose  |  Android Developers

CompositionLocal을 사용한 로컬 범위 지정 데이터 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. CompositionLocal은 암시적으로 컴포지션을 통해 데이터를 전달하

developer.android.com

Introducing 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을 제공합니다. CompositionLocal은 UI 트리를 통해 데이터가 흐르는 묵시적인 방법으로 사용할 수 있는 트리 범위의 명명된 객체를 생성할 수 있게 해줍니다.

CompositionLocal 요소는 일반적으로 UI 트리의 특정 노드에서 값을 제공합니다. 값은 해당 컴포저블 하위 요소가 composable 함수의 매개변수로 CompositionLocal 선언하지 않고 사용할 있습니다.

CompositionLocal은 Material 테마에서 내부적으로 사용됩니다. MaterialTheme은 컴포저블 구성의 하위 부분에서 나중에 그것들을 검색할 수 있게 해주는 세 가지 CompositionLocal 인스턴스인 colors, typography 및 shapes을 제공하는 객체입니다. 특히 이것들은 MaterialTheme colors, shapes 및 typography 속성을 통해 액세스할 수 있는 LocalColors, LocalShapes 및 LocalTypography 속성입니다.

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colors, 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.colors.primary
    )
}

CompositionLocal 인스턴스는 구성의 일부에 범위가 지정되어 있어 트리의 다른 수준에서 서로 다른 값을 제공할 수 있습니다. CompositionLocal의 현재 값은 해당 구성 부분에서 조상이 제공한 가장 가까운 값에 해당합니다.

CompositionLocal에 새 값을 제공하려면 CompositionLocalProvider와 해당 CompositionLocal 키를 값에 연결하는 provides 중위 함수를 사용합니다. CompositionLocalProvider의 콘텐츠 람다는 CompositionLocal의 현재 속성에 액세스할 때 제공된 값을 얻습니다. 새 값을 제공하면 Composition은 CompositionLocal을 읽는 구성의 일부를 다시 구성합니다.

예제의 경우 LocalContentAlpha CompositionLocal UI 다른 부분을 강조하거나 강조하지 않도록 텍스트 아이콘에 사용되는 선호하는 콘텐츠 알파 값을 포함합니다. 다음 예제에서는 CompositionLocalProvider 사용하여 구성의 다른 부분에 대해 서로 다른 값을 제공합니다.

@Composable
fun CompositionLocalExample() {
    MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the disabled alpha now")
}

위의 모든 예에서 CompositionLocal 인스턴스는 내부적으로 Material 컴포저블에서 사용되었습니다. 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)
}

Creating your own CompositionLocal

CompositionLocal은 데이터를 구성을 통해 암시적으로 아래로 전달하기 위한 도구입니다.

CompositionLocal을 사용해야 할 때의 또 다른 핵심적인 신호는 매개변수가 교차적이며 중간 구현 레이어가 존재하지 않아야 하는 경우입니다. 중간 레이어가 존재할 경우, 이러한 중간 레이어를 인식하게 되면 컴포저블의 유틸리티가 제한될 수 있습니다. 예를 들어, Android 권한에 대한 쿼리는 CompositionLocal에 의해 제공됩니다. 미디어 선택 컴포저블은 기존 API를 변경하지 않고 환경에서 사용되는 이 추가된 컨텍스트를 호출자에게 알리지 않아도 되게 하여 장치에서 보호된 내용에 액세스할 수 있는 새 기능을 추가할 수 있습니다.

그러나 CompositionLocal이 항상 최선의 해결책은 아닙니다. 우리는 CompositionLocal의 과도한 사용을 권장하지 않습니다. 이는 몇 가지 단점이 있기 때문입니다:

CompositionLocal은 컴포저블의 동작을 이해하기 어렵게 만듭니다. 묵시적 종속성을 생성하므로 이를 사용하는 컴포저블의 호출자는 모든 CompositionLocal에 대한 값을 충족시켜야 합니다.

또한 종속성에 대한 명확한 진리 출처가 없을 있습니다. 종속성은 구성의 어떤 부분에서든 변할 있으므로 문제가 발생한 경우 앱을 디버깅하는 것이 어려울 있습니다. IDE Find usages Compose 레이아웃 인스펙터와 같은 도구는 문제를 완화하기에 충분한 정보를 제공합니다.

Deciding whether to use CompositionLocal

CompositionLocal을 사용해야 하는 좋은 상황에는 다음과 같은 조건이 있습니다.

CompositionLocal은 좋은 기본 값을 가져야 합니다. 기본값이 없는 경우에는 CompositionLocal에 대한 값이 제공되지 않을 때 개발자가 어려운 상황에 처하게 되지 않도록 보장해야 합니다. 기본값을 제공하지 않으면 테스트를 생성하거나 해당 CompositionLocal을 사용하는 컴포저블을 미리 보는 경우 항상 명시적으로 값을 제공해야 할 필요가 있어 문제가 발생하고 답답함을 유발할 수 있습니다.

CompositionLocal은 트리 범위나 하위 계층 범위로 고려되지 않는 개념에는 사용하지 마세요. CompositionLocal은 잠재적으로 모든 하위 요소에 의해 사용될 수 있는 경우 의미가 있습니다. 일부 하위 요소에 의해 사용되는 경우가 아닌 경우에는 CompositionLocal을 피하세요.

이러한 요구 사항에 맞지 않는 경우에는 CompositionLocal을 만들기 전에 Alternatives to consider 섹션을 확인하세요.

좋지 않은 예로는 특정 화면의 ViewModel 보유하는 CompositionLocal 만들어 모든 스크린의 컴포저블이 ViewModel 대한 참조를 얻어 일부 논리를 수행할 있도록 하는 것입니다. 이것은 모든 특정 UI 트리 아래의 컴포저블이 ViewModel 대해 알아야 하는 것은 아닙니다. 좋은 예는 상태는 아래로 흐르고 이벤트는 위로 흐른다는 패턴을 따라 컴포저블에게 필요한 정보만 전달하는 것입니다. 접근 방식을 통해 컴포저블을 재사용 가능하게 만들고 테스트하기 쉽습니다.

Creating a CompositionLocal

CompositionLocal을 생성하기 위해 두 가지 API가 있습니다:

  • compositionLocalOf: 다시 구성될 때 제공된 값의 변경은 현재 값을 읽는 콘텐츠만 무효화합니다.
  • staticCompositionLocalOf: compositionLocalOf 달리 staticCompositionLocalOf 읽기는 Compose 의해 추적되지 않습니다. 값이 변경되면 CompositionLocal 제공되는 콘텐츠 람다 전체가 재구성됩니다. 현재 값이 구성에서 읽혀지는 위치만 재구성되는 것이 아니라 재구성됩니다.

만약 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() }

Providing values to a CompositionLocal

CompositionLocalProvider 컴포저블은 지정된 계층 구조에 대한 CompositionLocal 인스턴스에 값을 바인딩합니다. CompositionLocal 값을 제공하려면 다음과 같이 provides 중위 함수를 사용하여 CompositionLocal 키를 값에 연결하세요:

// 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
            }
        }
    }
}

Consuming the 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
    Card(elevation = LocalElevations.current.card) {
        // Content
    }
}

Alternatives to consider

CompositionLocal 일부 사용 사례에는 과도한 해결책일 있습니다. 사용 사례가 Deciding whether to use CompositionLocal 섹션에서 지정된 기준을 충족하지 않는 경우, 다른 해결책이 사용 사례에 적합할 있습니다.

Pass explicit parameters

컴포저블의 종속성을 명시적으로 지정하는 것은 좋은 습관입니다. 우리는 컴포저블에게 필요한 것만 전달하는 것을 권장합니다. 컴포저블을 분리하고 재사용하기 위해 컴포저블은 가능한 적은 양의 정보를 보유해야 합니다.

@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
}

Inversion of control

컴포저블에 불필요한 종속성을 전달하는 것을 피하는 또 다른 방법은 제어 역전을 통해 가능합니다. 하위 요소가 어떤 로직을 실행하기 위해 종속성을 가져오는 대신, 부모 요소가 그 역할을 대신 수행합니다.

다음 예시를 보세요. 하위 요소가 데이터를 로드하는 요청을 트리거해야 하는 경우:

@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()
    }
}

'Android > Compose Docs' 카테고리의 다른 글

Compose Docs | Custom layouts  (0) 2023.08.26
Compose Docs | Layout  (0) 2023.08.16
Compose Docs | Semantics in Compose  (0) 2023.08.13
Compose Docs | Performance  (0) 2023.08.13
Compose Docs | Jetpack Compose architectural layering  (0) 2023.08.10