Dane o zakresie lokalnym dzięki CompositionLocal

CompositionLocal to narzędzie do domyślnego przekazywania danych przez kompozycję. Na tej stronie znajdziesz bardziej szczegółowe informacje o tym, czym jest CompositionLocal, jak utworzyć własny projekt CompositionLocal. Dowiesz się też, czy CompositionLocal sprawdzi się w Twoim przypadku.

Przedstawiamy CompositionLocal

Zwykle w interfejsie Compose dane są przesyłane przez drzewo interfejsu jako parametry do każdej funkcji kompozycyjnej. To pozwala uwydatnić zależności kompozycyjnego. Może to jednak być kłopotliwe w przypadku danych, które są bardzo często i powszechnie używane, takich jak kolory czy style czcionki. Zobacz ten przykład:

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

Aby zapobiec konieczności przekazywania kolorów jako jawnej zależności parametrów do większości funkcji kompozycyjnych, funkcja Tworzenie funkcji CompositionLocal umożliwia tworzenie nazwanych obiektów o zakresie na poziomie drzewa, które można wykorzystać jako niejawny sposób przepływu danych przez drzewo interfejsu.

Elementy CompositionLocal mają zwykle przypisaną wartość w określonym węźle drzewa interfejsu. Tej wartości można używać przez jej elementy podrzędne z możliwością kompozycji bez deklarowania CompositionLocal jako parametru funkcji kompozycyjnej.

CompositionLocal to funkcja wykorzystywana w trybie Material Design. MaterialTheme to obiekt, który zawiera 3 wystąpienia CompositionLocal (kolory, typografia i kształty) – co umożliwia ich późniejsze pobranie w dowolnej podrzędnej części kompozycji. Są to właściwości LocalColors, LocalShapes i LocalTypography, do których możesz uzyskać dostęp za pomocą atrybutów MaterialTheme colors, shapes i typography.

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

Instancja CompositionLocal jest ograniczona do części kompozycji, więc możesz podać różne wartości na różnych poziomach drzewa. Wartość current parametru CompositionLocal odpowiada najbliższej wartości podanej przez element nadrzędny w tej części kompozycji.

Aby podać nową wartość w obiekcie CompositionLocal, użyj funkcji CompositionLocalProvider i jej funkcjiinfikacji provides, która wiąże klucz CompositionLocal z kluczem value. Lambda content obiektu CompositionLocalProvider otrzyma podaną wartość podczas uzyskiwania dostępu do właściwości current obiektu CompositionLocal. Gdy podasz nową wartość, funkcja Utwórz ponownie utworzy fragmenty kompozycji, które odczytują element CompositionLocal.

Na przykład w polu LocalContentAlpha CompositionLocal znajduje się preferowana treść w wersji alfa używana w przypadku tekstu i ikony, aby uwydatnić lub złagodzić różne elementy interfejsu. W poniższym przykładzie użyto funkcji CompositionLocalProvider do określenia różnych wartości dla różnych części kompozycji.

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

Rysunek 1. Podgląd funkcji kompozycyjnej CompositionLocalExample.

We wszystkich powyższych przykładach instancje CompositionLocal były wewnętrznie używane przez elementy kompozycyjne Material. Aby poznać bieżącą wartość właściwości CompositionLocal, użyj jej właściwości current. W poniższym przykładzie do formatowania tekstu używana jest bieżąca wartość Context elementu LocalContext CompositionLocal, która jest często używana w aplikacjach na Androida:

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

Tworzę Twój własny CompositionLocal.

CompositionLocal to narzędzie do domyślnego przekazywania danych przez kompozycję.

Kolejnym kluczowym sygnałem dotyczącym użycia CompositionLocal jest sytuacja, w której parametr ma charakter pośredni, a pośrednie warstwy implementacji nie powinny wiedzieć o istnieniu tego parametru, ponieważ uświadomienie w tych warstwach pośrednich ograniczenia przydatności funkcji kompozycyjnej. Na przykład wysyłanie zapytań o uprawnienia Androida jest przeprowadzane przy użyciu funkcji CompositionLocal. Element kompozycyjny selektora mediów może dodać nową funkcję, która zapewnia dostęp do treści chronionych uprawnieniami na urządzeniu bez zmiany interfejsu API i wymaga, aby elementy wywołujące selektor mediów miały świadomość tego dodatkowego kontekstu używanego ze środowiska.

Jednak CompositionLocal nie zawsze jest najlepszym rozwiązaniem. Odradzamy nadużywanie usługi CompositionLocal, ponieważ ma ona pewne wady:

CompositionLocal utrudnia racjonalne działanie funkcji kompozycyjnej. Tworząc pośrednie zależności, elementy wywołujące funkcje kompozycyjne, które z nich korzystają, muszą dbać o to, aby spełniony jest warunek każdego elementu CompositionLocal.

Poza tym zależność ta może zmieniać się w dowolnej części kompozycji, nie może więc nie być jasnego źródła informacji. Dlatego debugowanie aplikacji w przypadku wystąpienia problemu może być trudniejsze, ponieważ trzeba przejść do bardziej szczegółowego widoku kompozycji, aby sprawdzić, gdzie wpisano wartość current. Narzędzia takie jak wyszukiwanie użycia w IDE lub inspektor układu tworzenia wiadomości dostarczają wystarczającą ilość informacji do ograniczenia tego problemu.

Decydowanie, czy użyć aplikacji CompositionLocal

Oto pewne warunki, które sprawiają, że usługa CompositionLocal może być dobrym rozwiązaniem w Twoim przypadku:

CompositionLocal powinien mieć dobrą wartość domyślną. W przypadku braku wartości domyślnej musisz zagwarantować, że w sytuacji, w której deweloper nie ma podanej wartości CompositionLocal, może wyjątkowo trudno dotrzeć do celu. Brak wartości domyślnej może powodować problemy i frustrację podczas tworzenia testów lub wyświetlania podglądu funkcji kompozycyjnej, która korzysta z elementu CompositionLocal, który zawsze wymaga jego jawnego podania.

Unikaj wartości CompositionLocal w przypadku koncepcji, które nie są uważane za ograniczone do drzewa lub podhierarchii. Parametr CompositionLocal ma sens, jeśli może być potencjalnie używany przez dowolne elementy podrzędne, a nie kilka z nich.

Jeśli Twój przypadek użycia nie spełnia tych wymagań, przed utworzeniem CompositionLocal zapoznaj się z sekcją Alternatywy, które warto rozważyć.

Przykładem złej praktyki jest utworzenie elementu CompositionLocal zawierającego ViewModel konkretnego ekranu, tak aby wszystkie elementy kompozycyjne na tym ekranie mogły zawierać odwołanie do ViewModel w celu wykonania pewnych działań logicznych. Jest to dobra praktyka, ponieważ nie wszystkie funkcje kompozycyjne znajdujące się pod danym drzewem interfejsu muszą wiedzieć o elemencie ViewModel. Zalecaną praktyką jest przekazywanie do funkcji kompozycyjnych tylko tych informacji, których są niezbędne, zgodnie ze wzorcem przepływu stanu w dół, a zdarzeń w górę. Dzięki temu kompozycje będą bardziej przydatne i będą łatwiejsze do testowania.

Tworzę CompositionLocal

CompositionLocal można utworzyć za pomocą 2 interfejsów API:

  • compositionLocalOf: zmiana wartości podanej podczas zmiany kompozycji spowoduje, że tylko treść, która odczytuje jej wartość current, staje się nieprawidłowa.

  • staticCompositionLocalOf: W przeciwieństwie do elementu compositionLocalOf odczyty elementu staticCompositionLocalOf nie są śledzone przez funkcję tworzenia. Zmiana wartości powoduje ponowne utworzenie całej funkcji content, w której określono CompositionLocal, a nie tylko miejsc, w których jest odczytywana wartość current w kompozycji.

Jeśli wartość podana w polu CompositionLocal jest mało prawdopodobne lub nigdy się nie zmieni, użyj funkcji staticCompositionLocalOf, aby zwiększyć wydajność.

Na przykład system projektowania aplikacji może być oceniany w ten sposób, że elementy kompozycyjne są podnoszone za pomocą cienia komponentu UI. Różne wysokości aplikacji powinny być rozpowszechnione w całym drzewie interfejsu, więc używamy metody CompositionLocal. Wartość CompositionLocal jest generowana warunkowo na podstawie motywu systemu, więc używamy interfejsu API compositionLocalOf:

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

Podanie wartości w funkcji CompositionLocal

Funkcja kompozycyjna CompositionLocalProvider wiąże wartości z CompositionLocal instancji w danej hierarchii. Aby podać nową wartość w elemencie CompositionLocal, użyj funkcjiinfikacji provides, która wiąże klucz CompositionLocal z value w ten sposób:

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

Wykorzystanie CompositionLocal

Funkcja CompositionLocal.current zwraca wartość podaną przez najbliższy argument CompositionLocalProvider, który stanowi wartość tego parametru CompositionLocal:

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

Inne rozwiązania

W niektórych przypadkach użycie funkcji CompositionLocal może być zbyt długie. Jeśli Twój przypadek użycia nie spełnia kryteriów określonych w sekcji Decydowanie, czy użyć CompositionLocal, prawdopodobnie inne rozwiązanie może być lepiej dopasowane do Twojego przypadku.

Przekazuj parametry jawne

Wyraźne informowanie o zależnościach elementu kompozycyjnego to dobry nawyk. Zalecamy udostępnianie elementów kompozycyjnych tylko potrzebnych. Aby zachęcać do odseparowania i ponownego użycia elementów kompozycyjnych, każdy element kompozycyjny powinien zawierać jak najmniej informacji.

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

Odwrócenie kontroli

Innym sposobem uniknięcia przekazywania niepotrzebnych zależności do funkcji kompozycyjnej jest odwrócenie kontroli. Zamiast tego, aby element podrzędny działał logicznie, musi to robić.

Oto przykład, w którym element podrzędny musi aktywować żądanie wczytania danych:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

W zależności od przypadku MyDescendant może ponosić dużą odpowiedzialność. Poza tym brak zależności MyViewModel sprawia, że MyDescendant mniej czasu może zostać użyte ponownie, ponieważ są teraz połączone. Rozważmy alternatywę, która nie przekazuje zależności do elementu podrzędnego i korzysta z odwrócenia zasad kontroli, co sprawia, że element nadrzędny jest odpowiedzialny za wykonanie tej logiki:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

To podejście może się sprawdzić w niektórych przypadkach użycia, ponieważ odłącza element podrzędny od jego bezpośrednich elementów nadrzędnych. Elementy tego typu są zwykle bardziej złożone na rzecz bardziej elastycznych elementów kompozycyjnych niższego poziomu.

I podobnie, lambda funkcji @Composable można używać w ten sam sposób, aby uzyskać te same korzyści:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}