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, np. kolorów czy stylów 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 nie trzeba było przekazywać kolorów jako jawnej zależności parametru 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 używać 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 CompositionLocal odpowiada najbliższej wartości podanej przez element nadrzędny w tej części kompozycji.

Aby podać nową wartość dla CompositionLocal, użyj funkcji CompositionLocalProvider i jej funkcji w nawiasach 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 utworzy 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 kompozycji w sposób domyślny.

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 ograniczyłoby 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ć wyraźnego ź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ę w składowej, 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 zbadać 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. Brak wartości domyślnej może powodować problemy i frustrację podczas tworzenia testów lub podglądu komponentu, który używa tej funkcji. 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 funkcji 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
            }
        }
    }
}

Wykorzystywanie CompositionLocal

Funkcja CompositionLocal.current zwraca wartość podaną przez najbliższe wyrażenie CompositionLocalProvider, które dostarcza wartość dla argumentu 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

CompositionLocal może być nieodpowiednim rozwiązaniem w niektórych przypadkach użycia. 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 do 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 unikanie przekazywania niepotrzebnych zależności do komponentu jest inwersja kontroli. Zamiast tego, aby potomek przyjmował zależność do wykonania określonej logiki, wykonuje 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. Składniki nadrzędne stają się coraz bardziej złożone, ponieważ dają możliwość tworzenia bardziej elastycznych składników 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()
    }
}