CompositionLocal to narzędzie do przekazywania danych w kompozycji w sposób niejawny. Na tej stronie dowiesz się więcej o tym, czym jest CompositionLocal, jak utworzyć własny CompositionLocal oraz czy CompositionLocal jest dobrym rozwiązaniem w Twoim przypadku.
Wprowadzenie do CompositionLocal
Zwykle w Compose, dane przepływają w dół przez drzewo interfejsu jako parametry każdej funkcji kompozycyjnej. Dzięki temu zależności funkcji kompozycyjnej są jawne. Może to jednak być uciążliwe w przypadku danych, które są używane bardzo często i w wielu miejscach, takich jak kolory czy style typografii. 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 funkcji kompozycyjnych, Compose oferuje CompositionLocal, która umożliwia tworzenie nazwanych obiektów o zasięgu drzewa, które mogą być używane jako niejawny sposób przepływu danych przez drzewo interfejsu.
Elementy CompositionLocal zwykle mają wartość w określonym węźle drzewa interfejsu. Ta wartość może być używana przez jej funkcje typu „composable” potomne bez deklarowania CompositionLocal jako parametru w funkcji typu „composable”.
CompositionLocal jest tym, czego motyw Material używa pod maską.
MaterialTheme to obiekt, który udostępnia 3 instancje CompositionLocal: colorScheme, typography i shapes, co pozwala na ich późniejsze pobranie w dowolnej części kompozycji.
Są to właściwości LocalColorScheme, LocalShapes i LocalTypography, do których można uzyskać dostęp za pomocą atrybutów colorScheme, shapes i typography obiektu MaterialTheme.
@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 ) }
Instancja CompositionLocal jest ograniczona do części kompozycji , dzięki czemu możesz podawać różne wartości na różnych poziomach drzewa. Wartość current
obiektu CompositionLocal odpowiada najbliższej wartości podanej przez element
nadrzędny w tej części kompozycji.
Aby podać nową wartość dla CompositionLocal, użyj
CompositionLocalProvider
i jego funkcji wrostkowej provides
, która łączy klucz CompositionLocal z value. Lambda
content obiektu CompositionLocalProvider otrzyma podaną
wartość, gdy uzyska dostęp do właściwości current obiektu CompositionLocal. Gdy zostanie podana nowa wartość, Compose ponownie skomponuje części kompozycji, które odczytują CompositionLocal.
Na przykład LocalContentColor CompositionLocal zawiera
preferowany kolor treści używany w tekście i
ikonografii, aby zapewnić kontrast z bieżącym kolorem tła. W tym przykładzie 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") }
CompositionLocalExample.W ostatnim przykładzie instancje CompositionLocal były używane wewnętrznie przez funkcje kompozycyjne Material. Aby uzyskać dostęp do bieżącej wartości CompositionLocal,
użyj jej current
właściwości. W tym przykładzie do formatowania tekstu używana jest bieżąca wartość Context obiektu
LocalContext CompositionLocal, który jest powszechnie 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łasnego CompositionLocal
CompositionLocal to narzędzie do przekazywania danych w kompozycji w sposób niejawny.
Kolejnym kluczowym sygnałem do użycia CompositionLocal jest sytuacja, gdy parametr jest
przekrojowy, a warstwy pośrednie implementacji nie powinny wiedzieć
o jego istnieniu, ponieważ ograniczyłoby to użyteczność funkcji kompozycyjnej. Na przykład zapytanie o uprawnienia Androida jest
obsługiwane przez CompositionLocal pod maską. Funkcja kompozycyjna selektora multimediów
może dodawać nowe funkcje umożliwiające dostęp do treści chronionych uprawnieniami na
urządzeniu bez zmiany interfejsu API i bez konieczności informowania wywołujących selektor multimediów o
tym dodatkowym kontekście używanym ze środowiska.
Jednak CompositionLocal nie zawsze jest najlepszym rozwiązaniem. Odradzamy nadmierne używanie CompositionLocal, ponieważ ma ono pewne wady:
CompositionLocal utrudnia zrozumienie zachowania funkcji kompozycyjnej. Ponieważ tworzą one niejawne zależności, wywołujący funkcje kompozycyjne, które ich używają, muszą się upewnić, że wartość każdego CompositionLocal jest spełniona.
Ponadto może nie być jasnego źródła prawdy dla tej zależności, ponieważ może się ona zmieniać w dowolnej części kompozycji. Dlatego debugowanie aplikacji w przypadku wystąpienia problemu może być trudniejsze , ponieważ musisz przejść w górę kompozycji, aby sprawdzić, gdzie została podana wartość current. Narzędzia takie jak Znajdź
użycia w IDE lub inspektor układu Compose zapewniają wystarczającą
ilość informacji, aby rozwiązać ten problem.
Decydowanie, czy używać CompositionLocal
Istnieją pewne warunki, które mogą sprawić, że CompositionLocal będzie dobrym rozwiązaniem w Twoim przypadku:
CompositionLocal powinien mieć dobrą wartość domyślną. Jeśli nie ma wartości domyślnej, musisz zagwarantować, że deweloperowi będzie niezwykle trudno znaleźć się w sytuacji, w której wartość CompositionLocal nie jest podana.
Niepodanie wartości domyślnej może powodować problemy i frustrację podczas tworzenia testów lub wyświetlania podglądu funkcji kompozycyjnej, która używa tego CompositionLocal, ponieważ zawsze będzie wymagać jawnego podania wartości.
Unikaj CompositionLocal w przypadku koncepcji, które nie są uważane za ograniczone do drzewa lub
podhierarchii. CompositionLocal ma sens, gdy może być potencjalnie używany przez dowolny element potomny, a nie tylko przez kilka z nich.
Jeśli Twój przypadek użycia nie spełnia tych wymagań, przed utworzeniem
CompositionLocal zapoznaj się z sekcją
Alternatywy do rozważenia.
Przykładem złej praktyki jest utworzenie CompositionLocal, który zawiera ViewModel konkretnego ekranu, aby wszystkie funkcje kompozycyjne na tym ekranie mogły uzyskać odniesienie do ViewModel w celu wykonania pewnej logiki. Jest to zła praktyka, ponieważ nie wszystkie funkcje kompozycyjne poniżej określonego drzewa interfejsu muszą znać ViewModel. Dobrą praktyką jest przekazywanie do funkcji kompozycyjnych tylko tych informacji
, których potrzebują, zgodnie z wzorcem, w którym stan przepływa w dół, a zdarzenia w górę. Dzięki temu funkcje kompozycyjne będą bardziej wielokrotnego użytku i łatwiejsze do testowania.
Tworzenie CompositionLocal
Do tworzenia CompositionLocal służą 2 interfejsy API:
compositionLocalOf: zmiana wartości podanej podczas ponownej kompozycji unieważnia tylko treść, która odczytuje jej wartośćcurrent.staticCompositionLocalOf: w przeciwieństwie docompositionLocalOfodczytystaticCompositionLocalOfnie są śledzone przez Compose. Zmiana wartości powoduje ponowne skomponowanie całej lambdycontent, w której podanoCompositionLocal, a nie tylko miejsc, w których wartośćcurrentjest odczytywana w kompozycji.
Jeśli wartość podana w CompositionLocal jest mało prawdopodobna lub nigdy się nie zmieni, użyj staticCompositionLocalOf, aby zwiększyć wydajność.
Na przykład system projektowania aplikacji może mieć określony sposób podnoszenia funkcji kompozycyjnych za pomocą cienia w przypadku komponentu interfejsu. Ponieważ różne podniesienia w aplikacji powinny propagować się w całym drzewie interfejsu, używamy CompositionLocal. Ponieważ wartość CompositionLocal jest wyprowadzana warunkowo na podstawie motywu systemu, 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 CompositionLocal
Funkcja kompozycyjna CompositionLocalProvider
wiąże wartości z instancjami CompositionLocal w danej
hierarchii. Aby podać nową wartość dla CompositionLocal, użyj funkcji wrostkowej
provides
, która łączy 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 } } } }
Używanie CompositionLocal
CompositionLocal.current zwraca wartość podaną przez najbliższy
CompositionLocalProvider, który podaje wartość dla tego 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 } }
Alternatywy do rozważenia
CompositionLocal może być nadmiernym rozwiązaniem w niektórych przypadkach. Jeśli Twój
przypadek użycia nie spełnia kryteriów określonych w sekcji Decydowanie, czy używać
CompositionLocal, inne rozwiązanie może być lepiej
dopasowane do Twojego przypadku.
Przekazywanie jawnych parametrów
Jawne określanie zależności funkcji kompozycyjnej to dobry nawyk. Zalecamy, aby funkcje kompozycyjne otrzymywały tylko to, czego potrzebują. Aby zachęcić do rozdzielania i ponownego używania funkcji kompozycyjnych, każda funkcja kompozycyjna powinna 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 sterowania
Innym sposobem na uniknięcie przekazywania niepotrzebnych zależności do funkcji kompozycyjnej jest użycie odwrócenia sterowania. Zamiast elementu potomnego, który przyjmuje zależność w celu wykonania pewnej logiki, robi to element nadrzędny.
Zobacz ten przykład, w którym element potomny musi wywołać żą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 mieć wiele obowiązków. Ponadto przekazywanie MyViewModel jako zależności sprawia, że MyDescendant jest mniej wielokrotnego użytku, ponieważ są teraz ze sobą powiązane. Rozważ alternatywę, która nie przekazuje zależności do elementu potomnego i używa zasad odwrócenia sterowania, dzięki czemu element nadrzędny jest odpowiedzialny za wykonanie 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 być lepiej dopasowane do niektórych przypadków użycia, ponieważ odłącza element podrzędny od jego bezpośrednich elementów nadrzędnych. Funkcje kompozycyjne nadrzędne stają się bardziej złożone, aby mieć bardziej elastyczne funkcje kompozycyjne niższego poziomu.
Podobnie lambdy treści @Composable mogą być używane 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() } }
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Anatomia motywu w Compose
- Używanie widoków w Compose
- Kotlin w Jetpack Compose