CompositionLocal – это инструмент для неявной передачи данных через композицию. На этой странице вы узнаете, что такое CompositionLocal, как создать собственный CompositionLocal и подходит ли CompositionLocal для вашего случая.
Общие сведения о CompositionLocal
Обычно в Compose данные передаются вниз по дереву пользовательского интерфейса в качестве параметров для каждой composable-функции. Это делает зависимости composable-функции явными. Однако это может быть неудобно для данных, которые используются очень часто и широко, например для цветов или стилей шрифтов. Ниже приведен пример.
@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 ) }
Чтобы не передавать цвета в качестве явной зависимости параметров большинству composable-функций, в Compose есть CompositionLocal. С помощью этого инструмента можно создавать именованные объекты в области дерева, которые позволяют неявно передавать данные по дереву пользовательского интерфейса.
Элементы CompositionLocal обычно имеют значение в определенном узле дерева интерфейса. Это значение может использоваться дочерними composable-функциями без объявления CompositionLocal в качестве параметра.
Основные термины. В этом руководстве используются термины Композиция, Дерево пользовательского интерфейса и Иерархия пользовательского интерфейса. Хотя в других руководствах они могут использоваться как взаимозаменяемые, у них разные значения:
- Композиция – это запись графа вызовов composable-функций.
- Дерево или иерархия пользовательского интерфейса – это дерево
LayoutNode, созданное, обновленное и поддерживаемое процессом компоновки.
CompositionLocal – это то, что используется в тематическом оформлении Material Design.
MaterialTheme – это объект, который предоставляет три экземпляра CompositionLocal: colorScheme, typography и shapes. Это позволяет вам получать их позже в любой дочерней части Composition.
В частности, это свойства LocalColorScheme, LocalShapes и LocalTypography, доступ к которым можно получить через атрибуты MaterialTheme, colorScheme, shapes и 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 ) }
Экземпляр CompositionLocal ограничен частью композиции, поэтому вы можете указывать разные значения на разных уровнях дерева. Значение current для CompositionLocal соответствует ближайшему значению, предоставленному предком в этой части композиции.
Чтобы предоставить новое значение для CompositionLocal, используйте CompositionLocalProvider и его инфиксную функцию provides, которая связывает ключ CompositionLocal с value. Лямбда-выражение content в CompositionLocalProvider получит указанное значение при доступе к свойству current в CompositionLocal. Когда предоставляется новое значение, Compose перекомпонует части композиции, которые считывают CompositionLocal.
Например, LocalContentColor CompositionLocal содержит предпочтительный цвет контента, используемый для текста и иконографики, чтобы обеспечить контраст с текущим цветом фона. В примере ниже CompositionLocalProvider используется для передачи разных значений в разные части композиции.
@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.В последнем примере экземпляры CompositionLocal использовались внутри composable-функций Material. Чтобы получить текущее значение CompositionLocal, используйте свойство current. В примере ниже для форматирования текста используется текущее значение Context элемента LocalContext CompositionLocal, который часто применяется в приложениях для Android.
@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) }
Создайте свою функцию: CompositionLocal
CompositionLocal – это инструмент для неявной передачи данных через Composition.
Ещё один важный сигнал для использования CompositionLocal – когда параметр является сквозным и промежуточные уровни реализации не должны знать о его существовании, поскольку это ограничило бы полезность composable-функции. Например, запрос разрешений Android выполняется с помощью CompositionLocal. Composable-функция инструмента выбора медиа может добавлять новые функции для доступа к защищенному разрешениями контенту на устройстве без изменения API и без необходимости для вызывающих сторон знать об этом дополнительном контексте, используемом из среды.
Однако CompositionLocal не всегда является лучшим решением. Мы не рекомендуем злоупотреблять CompositionLocal, поскольку у этого подхода есть ряд недостатков:
CompositionLocal усложняет понимание поведения composable-функции. Поскольку они создают неявные зависимости, вызывающие объекты composable-функций, которые их используют, должны убедиться, что значение для каждого CompositionLocal удовлетворено.
Кроме того, может не быть четкого источника истины для этой зависимости, поскольку она может мутировать в любой части композиции. Поэтому отладка приложения при возникновении проблемы может быть более сложной, поскольку вам нужно перемещаться вверх по композиции, чтобы увидеть, где было предоставлено значение current. Такие инструменты, как Поиск использования в IDE или инспектор макетов Compose, предоставляют достаточно информации для устранения этой проблемы.
Решите, нужно ли использовать CompositionLocal
CompositionLocal – хороший вариант для вас, если:
Для CompositionLocal должно быть задано подходящее значение по умолчанию. Если значения по умолчанию нет, вы должны сделать так, чтобы разработчику было крайне сложно не указать значение для CompositionLocal.
Если не указать значение по умолчанию, могут возникнуть проблемы при создании тестов или предварительном просмотре composable-функции, использующей CompositionLocal, поскольку его всегда нужно будет указывать явно.
Не используйте CompositionLocal для понятий, которые не считаются относящимися к дереву или поддереву. CompositionLocal имеет смысл, когда его потенциально может использовать любой потомок, а не несколько из них.
Если ваш сценарий использования не соответствует этим требованиям, ознакомьтесь с разделом Альтернативные варианты, прежде чем создавать CompositionLocal.
Примером неправильного подхода является создание CompositionLocal, который содержит ViewModel определенного экрана, чтобы все composable-функции на этом экране могли получить ссылку на ViewModel для выполнения некоторой логики. Это плохая практика, потому что не всем composable-функциям ниже определенного дерева пользовательского интерфейса нужно знать о ViewModel. Рекомендуется передавать в composable-функции только ту информацию, которая им нужна, следуя шаблону, согласно которому состояние передается вниз, а события – вверх.
Такой подход позволит вам повторно использовать composable-функции и упростит тестирование.
Создать CompositionLocal
Создать CompositionLocal можно с помощью двух API:
compositionLocalOf. Изменение значения, предоставленного во время рекомпозиции, делает недействительным только контент, который считывает значениеcurrent.staticCompositionLocalOf. В отличие отcompositionLocalOf, чтениеstaticCompositionLocalOfне отслеживается Compose. Изменение значения приводит к рекомпозиции всей лямбда-функцииcontent, в которой указан параметрCompositionLocal, а не только тех мест, где значениеcurrentсчитывается в композиции.
Если значение, передаваемое в CompositionLocal, вряд ли изменится или не изменится никогда, используйте staticCompositionLocalOf, чтобы повысить производительность.
Например, в системе проектирования приложения может быть указано, что composable-функции должны быть приподняты с помощью тени для компонента пользовательского интерфейса. Поскольку разные высоты для приложения должны распространяться по всему дереву пользовательского интерфейса, мы используем CompositionLocal. Поскольку значение CompositionLocal определяется условно на основе темы системы, мы используем 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() }
Укажите значения для CompositionLocal
Composable-функция CompositionLocalProvider связывает значения с экземплярами CompositionLocal для заданной иерархии. Чтобы задать новое значение для CompositionLocal, используйте инфиксную функцию provides, которая связывает ключ CompositionLocal с value следующим образом:
// 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 } } } }
Работа с CompositionLocal
CompositionLocal.current возвращает значение, предоставленное ближайшим CompositionLocalProvider, который предоставляет значение для этого 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 } }
Альтернативные варианты
В некоторых случаях CompositionLocal может быть избыточным решением. Если ваш вариант использования не соответствует критериям, указанным в разделе Решение о том, использовать ли CompositionLocal, возможно, для вашего варианта использования лучше подойдет другое решение.
Передача явных параметров
Явное указание зависимостей composable-функции – хорошая привычка. Рекомендуем передавать composable-функциям только то, что им нужно. Чтобы поощрять разделение и повторное использование composable-функций, каждая из них должна содержать как можно меньше информации.
@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 }
Инверсия управления
Ещё один способ избежать передачи ненужных зависимостей в composable-функцию – использовать инверсию управления. Вместо того, чтобы потомок принимал зависимость для выполнения некоторой логики, это делает родитель.
В примере ниже дочерний элемент должен запустить запрос на загрузку данных.
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
В зависимости от ситуации у MyDescendant может быть много обязанностей. Кроме того, передача MyViewModel в качестве зависимости делает MyDescendant менее пригодным для повторного использования, поскольку теперь они связаны друг с другом. Рассмотрим альтернативный вариант, в котором зависимость не передается в дочерний элемент, а используется принцип инверсии управления, который возлагает на родительский элемент ответственность за выполнение логики:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
Этот подход может лучше подходить для некоторых случаев, поскольку он отделяет дочерний элемент от его непосредственных предков. Предки composable-функций становятся сложнее, чтобы обеспечить гибкость дочерних элементов.
Аналогичным образом можно использовать лямбда-выражения для контента @Composable, чтобы получить те же преимущества:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Структура темы в Compose
- Использование представлений в Compose
- Kotlin для Jetpack Compose