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 funkcji Compose dane przepływają w dół Drzewo interfejsu jako parametry każdej funkcji kompozycyjnej. Dzięki temu zależności kompozytowe są jawne. Może to jednak być niewygodne w przypadku danych, które często i powszechnie stosowane, takie jak kolory czy styl pisania. Zobacz poniższe informacje 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 wykorzystać jako domyślny sposób przepływu danych przez drzewo interfejsu użytkownika.

CompositionLocal elementy 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 zawiera 3 instancje CompositionLocal: colorScheme, typography i shapes, dzięki czemu możesz je później pobrać w dowolnym elemencie podrzędnym. są częścią 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 CompositionLocal odpowiada najbliższej wartości podanej przez elementu nadrzędnego w danej 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 skompiluje części kompozycji, które odczytują CompositionLocal.

Na przykład pole LocalContentColor CompositionLocal zawiera preferowany kolor treści używany w przypadku tekstu i ikony, które kontrastują 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 w postaci plików kompozycyjnych Material. Aby uzyskać dostęp do bieżącej wartości elementu CompositionLocal, użyj właściwości current. W poniższym przykładzie bieżąca wartość Context parametru LocalContext Język CompositionLocal, który jest często używany w aplikacjach na Androida, jest używany do formatowania tekst:

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

Innym ważnym sygnałem dotyczącym używania CompositionLocal jest sytuacja, w której parametr ma postać a także krzyżowe i pośrednie warstwy implementacji istnieje, ponieważ informowanie tych warstw pośrednich ograniczyłoby z rodzaju kompozycyjnego. Na przykład wysyłanie zapytań o uprawnienia Androida to który zapewnia 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. Śr Zniechęcaj do nadużywania funkcji CompositionLocal, ponieważ ma ona pewne wady:

CompositionLocal utrudnia analizowanie zachowania komponentu. Ponieważ wywołania takich komponentów tworzą ukryte zależności, wywołujący komponenty, które ich używają, muszą się upewnić, że wartość dla 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 przy w których występują problemy, ponieważ trzeba zapoznać się z 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 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 tekstu CompositionLocal w przypadku pojęć, które nie są ograniczone do drzewa lub w hierarchii podrzędnej. Atrybut CompositionLocal ma sens, kiedy może być mogą być wykorzystywane przez wszystkie elementy podrzędne, a nie tylko niektóre 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. To zła praktyka bo nie wszystkie elementy kompozycyjne znajdujące się poniżej danego drzewa UI muszą wiedzieć o 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 łatwiej je będzie testować.

Tworzę CompositionLocal

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

  • compositionLocalOf: Zmiana wartości podanej podczas zmiany kompozycji powoduje unieważnienie tylko tego, treść, która czyta się current .

  • staticCompositionLocalOf: W przeciwieństwie do compositionLocalOf odczyty elementu staticCompositionLocalOf nie są śledzić przez funkcję tworzenia wiadomości. 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. Ponieważ różne poziomy aplikacji powinny być propagowane w drzewie interfejsu, używamy elementu 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ść w polu CompositionLocal, użyj funkcji provides infix, 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

CompositionLocal.current zwraca wartość podaną przez najbliższy punkt CompositionLocalProvider, który podaje wartość argumentowi 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 zastosowanie parametru CompositionLocal może być nadmiarowe. Jeśli nie spełnia kryteriów określonych w Podejmowaniu decyzji, czy CompositionLocal, inne rozwiązanie może być lepsze. do Twoich potrzeb.

Przekazywanie parametrów jawnych

Wyraźne informowanie o zależnościach funkcji kompozycyjnej to dobry nawyk. 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, że element podrzędny przyjmuje zależność wykonaj jakąś logikę, a element nadrzędny.

Zapoznaj się z przykładem poniżej, w którym element podrzędny musi wyzwalać żądanie wczytaj trochę 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 na firmie MyDescendant spoczywa duża odpowiedzialność. Ponadto przekazanie 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 się sprawdzić w niektórych przypadkach użycia, ponieważ rozdziela dziecko z jego bezpośrednich przodków. Składniki nadrzędne stają się coraz bardziej złożone, ponieważ mają bardziej elastyczne składniki niższego poziomu.

Podobnie lambda lambda dotycząca treści (@Composable) może być używana w taki sam sposób, 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()
    }
}