CompositionLocal to narzędzie do
przekazywania danych w dół przez kompozycję w sposób dorozumiany. Na tej stronie dowiesz się więcej o CompositionLocal, jak utworzyć własny CompositionLocal i czy CompositionLocal jest dobrym rozwiązaniem w Twoim przypadku.
Wprowadzenie do CompositionLocal
Zwykle w Compose dane przepływają w dół drzewa interfejsu jako parametry każdej funkcji typu „composable”. Dzięki temu zależności funkcji kompozycyjnej są jawne. Może to jednak być uciążliwe w przypadku danych, które są bardzo często i powszechnie używane, np. kolorów lub stylów czcionek. Przyjrzyj się temu przykładowi:
@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 oferuje CompositionLocal, co umożliwia tworzenie nazwanych obiektów o zakresie 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 elementy podrzędne typu „composable” bez deklarowania CompositionLocal jako parametru w funkcji typu „composable”.
Kluczowe terminy: w tym przewodniku używamy terminów kompozycja, drzewo interfejsu i hierarchia interfejsu. Chociaż w innych przewodnikach mogą być używane zamiennie, mają różne znaczenia:
- Kompozycja to zapis grafu wywołań funkcji typu „composable”.
- Drzewo interfejsu lub hierarchia interfejsu to drzewo
LayoutNode, które jest tworzone, aktualizowane i utrzymywane przez proces kompozycji.
CompositionLocal to element, którego motyw Material używa w swoim działaniu.
MaterialTheme to obiekt, który udostępnia 3 instancje CompositionLocal: colorScheme, typography i shapes. Możesz je później pobrać w dowolnej części podrzędnej kompozycji.
Są to właściwości LocalColorScheme, LocalShapes i LocalTypography, do których możesz uzyskać dostęp za pomocą atrybutów MaterialTheme
colorScheme, shapes i typography.
@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, więc możesz podawać różne wartości na różnych poziomach drzewa. Wartość current elementu 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 funkcji wrostkowej provides, która przypisuje klucz CompositionLocal do value. Funkcja lambda content w przypadku CompositionLocalProvider otrzyma podaną wartość podczas uzyskiwania dostępu do właściwości current elementu CompositionLocal. Gdy podana zostanie 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 przypadku tekstu i ikon, aby zapewnić kontrast z bieżącym kolorem tła. W poniższym przykładzie symbol 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 komponenty kompozycyjne Material. Aby uzyskać dostęp do bieżącej wartości CompositionLocal, użyj właściwości current. W tym przykładzie do sformatowania tekstu użyto bieżącej wartości Context LocalContext CompositionLocal, która jest powszechnie stosowana 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) }
Utwórz własne: CompositionLocal
CompositionLocal to narzędzie do przekazywania danych w dół przez kompozycję w sposób niejawny.
Kolejnym ważnym sygnałem do użycia CompositionLocal jest sytuacja, w której parametr jest przekrojowy, a warstwy pośrednie implementacji nie powinny wiedzieć o jego istnieniu, ponieważ uświadomienie im tego ograniczyłoby użyteczność funkcji kompozycyjnej. Na przykład zapytanie o uprawnienia Androida jest realizowane za pomocą CompositionLocal. Kompozycja 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 wymagania, aby wywołujący selektor multimediów mieli świadomość tego dodatkowego kontekstu używanego w środowisku.
Jednak CompositionLocal nie zawsze jest najlepszym rozwiązaniem. Odradzamy nadmierne korzystanie z CompositionLocal, ponieważ ma to pewne wady:
CompositionLocal utrudnia zrozumienie działania funkcji kompozycyjnej. Tworzą one zależności niejawne, więc wywołujący funkcje kompozycyjne, które ich używają, muszą się upewnić, że każda wartość CompositionLocal jest spełniona.
Ponadto może nie być jasnego źródła informacji o tej zależności, ponieważ może się ona zmieniać w dowolnej części kompozycji. Dlatego debugowanie aplikacji, gdy wystąpi problem, może być trudniejsze, ponieważ musisz przejść w górę kompozycji, aby sprawdzić, gdzie podano wartość current. Narzędzia takie jak Find usages (Znajdź użycia) w IDE lub inspektor układu Compose dostarczają wystarczająco dużo informacji, aby rozwiązać ten problem.
Decydowanie o użyciu CompositionLocal
Istnieją pewne warunki, które mogą sprawić, że CompositionLocal będzie dobrym rozwiązaniem w Twoim przypadku:
CompositionLocal powinna 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 nie podano wartości parametru CompositionLocal.
Brak wartości domyślnej może powodować problemy i frustrację podczas tworzenia testów lub wyświetlania podglądu komponentu, który używa tego elementu CompositionLocal i zawsze wymaga jego wyraźnego podania.
Unikaj używania CompositionLocal w przypadku pojęć, które nie są uważane za zakres drzewa lub zakres podhierarchii. CompositionLocal ma sens, gdy może być potencjalnie używany przez wszystkich potomków, a nie tylko przez kilku z nich.
Jeśli Twój przypadek użycia nie spełnia tych wymagań, przed utworzeniem CompositionLocal zapoznaj się z sekcją Alternatywne rozwiązania.
Przykładem nieodpowiedniej metody jest utworzenie CompositionLocal, które zawiera ViewModel konkretnego ekranu, aby wszystkie komponenty kompozycyjne na tym ekranie mogły uzyskać odwołanie do ViewModel i wykonać pewną logikę. To zła praktyka, ponieważ nie wszystkie funkcje kompozycyjne w określonym drzewie interfejsu muszą znać wartość ViewModel. Sprawdzoną metodą jest przekazywanie do funkcji kompozycyjnych tylko tych informacji, które są im potrzebne, zgodnie z wzorcem stan przepływa w dół, a zdarzenia w górę.
Dzięki temu Twoje funkcje kompozycyjne będą bardziej wielokrotnego użytku i łatwiejsze do testowania.
Utwórz CompositionLocal
Do tworzenia CompositionLocal służą 2 interfejsy API:
compositionLocalOf: zmiana wartości podanej podczas rekompozycji unieważnia tylko treść, która odczytuje jej wartość.currentstaticCompositionLocalOf: W przeciwieństwie docompositionLocalOfodczytystaticCompositionLocalOfnie są śledzone przez Compose. Zmiana wartości powoduje ponowne skomponowanie całej funkcji lambdacontent, w której podano wartośćCompositionLocal, a nie tylko miejsc, w których w kompozycji odczytywana jest wartośćcurrent.
Jeśli wartość przekazywana do CompositionLocal raczej się nie zmieni 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 komponencie interfejsu. Różne poziomy uniesienia aplikacji powinny być propagowane w całym drzewie interfejsu, dlatego używamy CompositionLocal. Wartość CompositionLocal jest uzyskiwana warunkowo na podstawie motywu systemu, dlatego używamy interfejsu compositionLocalOf API:
// 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() }
Podaj wartości dla CompositionLocal
Funkcja CompositionLocalProvider
composable wiąże wartości z instancjami CompositionLocal w danej
hierarchii. Aby podać nową wartość dla CompositionLocal, użyj funkcji infiksowej provides, która przypisuje klucz CompositionLocal do wartości 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 } } } }
Przetwarzanie CompositionLocal
CompositionLocal.current zwraca wartość podaną przez najbliższy element CompositionLocalProvider, który dostarcza wartość do elementu 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
W niektórych przypadkach użycia CompositionLocal może być zbyt rozbudowanym rozwiązaniem. Jeśli Twój przypadek użycia nie spełnia kryteriów określonych w sekcji Decydujemy, czy użyć CompositionLocal, inne rozwiązanie może być lepiej dopasowane do Twojego przypadku użycia.
Przekazywanie parametrów jawnych
Jawne określanie zależności funkcji kompozycyjnych to dobry nawyk. Zalecamy przekazywanie do funkcji kompozycyjnych tylko tego, czego potrzebują. Aby zachęcić do rozdzielania i ponownego używania funkcji kompozycyjnych, każda z nich 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 kontroli
Innym sposobem na uniknięcie przekazywania do funkcji kompozycyjnej niepotrzebnych zależności jest użycie odwrócenia sterowania. Zamiast tego, aby wykonać jakąś logikę, element podrzędny pobiera zależność, a element nadrzędny.
Poniższy przykład pokazuje, jak element podrzędny może 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 ponosić dużą odpowiedzialność. Ponadto przekazywanie MyViewModel jako zależności sprawia, że MyDescendant jest mniej wielokrotnego użytku, ponieważ są one teraz ze sobą powiązane. Rozważ alternatywę, która nie przekazuje zależności do elementu podrzędnego i korzysta z zasad odwrócenia sterowania, dzięki czemu element nadrzędny 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") } }
W niektórych przypadkach może to być lepsze rozwiązanie, ponieważ odłącza element podrzędny od jego bezpośrednich elementów nadrzędnych. Komponenty kompozycyjne przodków stają się bardziej złożone, aby zapewnić większą elastyczność komponentów kompozycyjnych niższego poziomu.
Podobnie można używać lambd treści @Composable, 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.
- Składnia motywu w Compose
- Używanie widoków w Compose
- Kotlin w Jetpack Compose