Dane o zakresie lokalnym dzięki CompositionLocal

CompositionLocal to narzędzie do przekazywania danych w składowej w sposób domyślny. Z tej strony dowiesz się więcej o tym, czym jest CompositionLocal, jak utworzyć własną CompositionLocal i czy CompositionLocal to dobre rozwiązanie w Twoim przypadku.

Przedstawiamy CompositionLocal

Zwykle w Compose dane przepływają w dół przez drzewo interfejsu użytkownika jako parametry każdej funkcji kompozytowej. Dzięki temu zależności kompozytowe są jawne. Może to jednak być uciążliwe w przypadku danych, które są często i powszechnie używane, takich jak kolory czy style czcionek. 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 uniknąć konieczności przekazywania kolorów jako jawnej zależności parametrycznej do większości komponentów, Compose udostępnia parametr CompositionLocal, który umożliwia tworzenie obiektów nazwanych w zakresie drzewa, które można wykorzystać jako domyślny sposób przepływu danych przez drzewo interfejsu użytkownika.

Elementy CompositionLocal są zwykle dostarczane z wartością w określonym węźle drzewa interfejsu. Ta wartość może być używana przez potomków funkcji składanej bez deklarowania CompositionLocal jako parametru w funkcji składanej.

CompositionLocal to to, czego używa motyw Material. MaterialTheme to obiekt, który udostępnia 3 instancje CompositionLocal: colorScheme, typographyshapes, co pozwala na ich późniejsze pobieranie w dowolnej części potomnej kompozycji. Dotyczy to w szczególności właściwości LocalColorScheme, LocalShapesLocalTypography, do których można uzyskać dostęp za pomocą atrybutów MaterialTheme, colorScheme, shapestypography.

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

Występ CompositionLocal jest ograniczony do części kompozycji, dzięki czemu możesz podawać różne wartości na różnych poziomach drzewa. Wartość current w elemencie CompositionLocal odpowiada wartości najbliższej wartości podanej przez element nadrzędny w tej części składu.

Aby podać nową wartość dla CompositionLocal, użyj funkcji CompositionLocalProvider i jej funkcji wstawienia provides, która łączy klucz CompositionLocal z value. Lambda content obiektu CompositionLocalProvider otrzyma podawaną wartość podczas uzyskiwania dostępu do właściwości current obiektu CompositionLocal. Gdy podasz nową wartość, Compose ponownie skompiluje części kompozycji, które odczytują CompositionLocal.

Przykładowo atrybuty LocalContentColor CompositionLocal zawierają preferowany kolor treści używany do tekstu i ikonografii, aby zapewnić kontrast z bieżącym kolorem tła. W tym przykładzie zmienna CompositionLocalProvider służy do podawania różnych wartości w różnych częściach kompozycji.

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

Rysunek 1. Podgląd komponentu CompositionLocalExample.

W ostatnim przykładzie instancje CompositionLocal były używane wewnętrznie przez komponenty Material. Aby uzyskać dostęp do bieżącej wartości elementu CompositionLocal, użyj właściwości current. W tym przykładzie do formatowania tekstu użyto bieżącej wartości Context parametru LocalContext CompositionLocal, który jest często używany 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)
}

Tworzenie własnych CompositionLocal

CompositionLocal to narzędzia do przekazywania danych w ramach usługi Composed implicitnie.

Kolejnym sygnałem wskazującym na użycie CompositionLocal jest sytuacja, w której parametr jest elementem pośrednim, a warstwy pośrednie implementacji nie powinny o nim wiedzieć, ponieważ ujawnienie tej informacji tym warstwom ograniczy użyteczność komponentu. Na przykład zapytanie o uprawnienia Androida jest obsługiwane przez CompositionLocal. Komponent selektora multimediów może dodawać nowe funkcje, aby uzyskać dostęp do treści chronionych hasłem na urządzeniu bez zmiany interfejsu API i wymagania od wywołujących selektor multimediów znajomości tego dodanego kontekstu używanego w środowisku.

Jednak CompositionLocal nie zawsze jest najlepszym rozwiązaniem. Nie zalecamy nadużywania funkcji CompositionLocal, ponieważ ma ona pewne wady:

CompositionLocal utrudnia analizowanie zachowania komponentu. Ponieważ wywołania komponentów, które ich używają, tworzą ukryte zależności, wywołujący muszą się upewnić, że wartość każdego CompositionLocal jest spełniona.

Ponadto może nie być jasnego źródła informacji o tej zależności, ponieważ może ona ulec zmianie w dowolnej części Kompozycji. Dlatego debugowanie aplikacji w przypadku wystąpienia problemu może być trudniejsze, ponieważ musisz przejść w górę do sekcji Kompozycja, aby sprawdzić, gdzie podano wartość current. Narzędzia takie jak Znajdź użycie w IDE lub Inspektor układu w Compose zawierają wystarczającą ilość informacji, aby rozwiązać ten problem.

Decydowanie o użyciu CompositionLocal

W pewnych przypadkach CompositionLocal może być dobrym rozwiązaniem:

Parametr CompositionLocal powinien mieć odpowiednią wartość domyślną. Jeśli nie ma wartości domyślnej, musisz zagwarantować, że bardzo trudno będzie deweloperowi znaleźć się w sytuacji, w której nie podano wartości parametru CompositionLocal. Niepodanie wartości domyślnej może spowodować problemy i niezadowolenie podczas tworzenia testów lub podglądu kompozytowego, który używa tej wartości. CompositionLocal będzie zawsze wymagać podania tej wartości.

Unikaj CompositionLocal w przypadku pojęć, które nie są ograniczone do drzewa lub podhierarchii. CompositionLocal ma sens, gdy może być potencjalnie używany przez dowolnego potomka, a nie tylko przez kilku z nich.

Jeśli Twoje zastosowanie nie spełnia tych wymagań, przed utworzeniem CompositionLocal zapoznaj się z sekcją Możliwe rozwiązania.

Przykładem nieodpowiedniej metody jest tworzenie CompositionLocal, które zawiera ViewModel danego ekranu, aby wszystkie komponenty na tym ekranie mogły uzyskać odwołanie do ViewModel w celu wykonania pewnych operacji. Jest to zła praktyka, ponieważ nie wszystkie komponenty w drzewie interfejsu użytkownika muszą znać wartość ViewModel. Dobrą praktyką jest przekazywanie komponentom tylko tych informacji, których potrzebują, zgodnie ze schematem, w którym stan przepływa w dół, a zdarzenia w górę. Dzięki temu komponenty będą bardziej przydatne i łatwiejsze do testowania.

Tworzenie CompositionLocal

Do tworzenia CompositionLocal można użyć 2 interfejsów API:

  • compositionLocalOf: zmiana wartości podawanej podczas rekompozycji unieważnia tylko treści, które odczytują wartośćcurrent.

  • staticCompositionLocalOf: w odróżnieniu od compositionLocalOf, usługa Compose nie śledzi odczytów staticCompositionLocalOf. Zmiana wartości powoduje ponowne złożenie całej funkcji lambda content, w której podana jest wartość CompositionLocal, a nie tylko miejsc, w których wartość current jest odczytywana w kompozycji.

Jeśli wartość podana w polu CompositionLocal jest mało prawdopodobna do zmiany lub nigdy się nie zmieni, użyj pola staticCompositionLocalOf, aby uzyskać korzyści związane ze skutecznością.

Na przykład system projektowania aplikacji może być subiektywny w sposobie, w jaki komponenty składane są wyróżniane za pomocą cienia dla komponentu interfejsu użytkownika. Różne poziomy aplikacji powinny być propagowane w drzewie interfejsu użytkownika, dlatego używamy CompositionLocal. Ponieważ wartość CompositionLocal jest wyprowadzana warunkowo na podstawie motywu systemowego, 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() }

Podawanie wartości w obszarze CompositionLocal

Komponent CompositionLocalProviderprzypisuje wartości do instancji CompositionLocal w danej hierarchii. Aby podać nową wartość CompositionLocal, użyj funkcji providesinfix, która wiąże klucz CompositionLocal z elementem 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
            }
        }
    }
}

Korzystanie z CompositionLocal

Funkcja CompositionLocal.current zwraca wartość podaną przez najbliższe wyrażenie CompositionLocalProvider, które dostarcza wartość dla tego wyrażenia CompositionLocal:

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

Możliwe rozwiązania

W niektórych przypadkach CompositionLocal może być rozwiązaniem nieadekwatnym. Jeśli Twój przypadek użycia nie spełnia kryteriów określonych w sekcji Decydowanie, czy użyć CompositionLocal, inne rozwiązanie może być lepsze dla Twojego przypadku użycia.

Przekazywanie parametrów jawnych

Dobrą praktyką jest wyraźne określenie zależności komponentów. Zalecamy, aby przekazywać komponentom tylko to, czego potrzebują. Aby zachęcić do rozdzielania i ponownego używania komponentów, każdy z nich 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 na uniknięcie przekazywania niepotrzebnych zależności do kompozytowalności jest inwersja kontroli. Zamiast tego, aby potomek przyjmował zależność do wykonania określonej logiki, robi to element nadrzędny.

W tym przykładzie potomek musi wywołać żądanie, aby załadować dane:

@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 sytuacji MyDescendant może mieć wiele obowiązków. Ponadto przekazywanie MyViewModel jako zależności powoduje, że MyDescendant jest mniej użyteczny, ponieważ te elementy są teraz ze sobą połączone. Rozważ rozwiązanie, które nie przekazuje zależności potomkowi i korzysta z inwersji zasad kontroli, dzięki której rodzic jest odpowiedzialny za wykonywanie logiki:

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

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

Takie podejście może być odpowiednie w niektórych przypadkach, ponieważ odłącza dziecko od jego bezpośrednich przodków. Elementy składowe przodków stają się coraz bardziej złożone, ponieważ mają bardziej elastyczne elementy składowe niż te na niższych poziomach.

Podobnie @Composablelambda treści może być używana w taki 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()
    }
}