CompositionLocal
— это инструмент для неявной передачи данных через Composition. На этой странице вы более подробно узнаете, что такое CompositionLocal
, как создать свой собственный CompositionLocal
и узнаете, является ли CompositionLocal
хорошим решением для вашего варианта использования.
Представляем CompositionLocal
Обычно в Compose данные проходят через дерево пользовательского интерфейса в виде параметров каждой компонуемой функции. Это делает зависимости компонуемого объекта явными. Однако это может оказаться затруднительным для данных, которые очень часто и широко используются, таких как цвета или стили шрифтов. См. следующий пример:
@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 ) }
Чтобы избежать необходимости передавать цвета в качестве явной зависимости параметра для большинства составных объектов, Compose предлагает CompositionLocal
, который позволяет создавать именованные объекты в области дерева, которые можно использовать в качестве неявного способа потока данных через дерево пользовательского интерфейса.
Элементам CompositionLocal
обычно присваивается значение в определенном узле дерева пользовательского интерфейса. Это значение может использоваться его составными потомками без объявления CompositionLocal
в качестве параметра в составной функции.
CompositionLocal
— это то, что тема Material использует под капотом. 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") }
Рисунок 1. Предварительный просмотр компонуемого объекта CompositionLocalExample
.
В последнем примере экземпляры CompositionLocal
использовались внутри компонуемых материалов 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
— это когда параметр является сквозным и промежуточные уровни реализации не должны знать о его существовании , поскольку информирование этих промежуточных уровней ограничит полезность составного элемента. Например, запрос разрешений Android осуществляется с помощью CompositionLocal
. Компонуемый сборщик мультимедиа может добавлять новые функции для доступа к контенту, защищенному разрешениями, на устройстве, не меняя его API и не требуя, чтобы вызывающие средства выбора медиа знали об этом добавленном контексте, используемом из среды.
Однако CompositionLocal
не всегда является лучшим решением. Мы не рекомендуем злоупотреблять CompositionLocal
поскольку у него есть некоторые недостатки:
CompositionLocal
усложняет понимание поведения компонуемого объекта . Поскольку они создают неявные зависимости, вызывающие их компоненты должны убедиться, что значение для каждого CompositionLocal
удовлетворено.
Более того, для этой зависимости может не быть четкого источника истины, поскольку она может мутировать в любой части композиции. Таким образом, отладка приложения при возникновении проблемы может быть более сложной, поскольку вам нужно перемещаться вверх по композиции, чтобы увидеть, где было предоставлено current
значение. Такие инструменты, как «Найти использование в среде IDE» или «Инспектор компоновки компоновки», предоставляют достаточно информации для устранения этой проблемы.
Решение о том, использовать ли CompositionLocal
Существуют определенные условия, которые могут сделать CompositionLocal
хорошим решением для вашего варианта использования:
CompositionLocal
должен иметь хорошее значение по умолчанию . Если значения по умолчанию нет, вы должны гарантировать, что разработчику будет чрезвычайно сложно попасть в ситуацию, когда значение для CompositionLocal
не указано. Отсутствие значения по умолчанию может вызвать проблемы и разочарования при создании тестов или предварительном просмотре составного объекта, который использует этот CompositionLocal
всегда будет требовать его явного предоставления.
Избегайте CompositionLocal
для концепций, которые не рассматриваются как области дерева или подиерархии . CompositionLocal
имеет смысл, когда его потенциально может использовать любой потомок, а не несколько из них.
Если ваш вариант использования не соответствует этим требованиям, ознакомьтесь с разделом «Альтернативы для рассмотрения», прежде чем создавать CompositionLocal
.
Примером плохой практики является создание CompositionLocal
, который содержит ViewModel
определенного экрана, чтобы все составные элементы на этом экране могли получить ссылку на ViewModel
для выполнения некоторой логики. Это плохая практика, потому что не все составные элементы ниже определенного дерева пользовательского интерфейса должны знать о ViewModel
. Хорошей практикой является передача компонуемым объектам только той информации, которая им нужна, следуя шаблону, согласно которому состояние течет вниз, а события — вверх . Такой подход сделает ваши составные элементы более пригодными для повторного использования и более простыми для тестирования.
Создание CompositionLocal
Существует два API для создания CompositionLocal
:
compositionLocalOf
: изменение значения, предоставленного во время рекомпозиции, делает недействительным только тот контент, который считывает своеcurrent
значение.staticCompositionLocalOf
: в отличие отcompositionLocalOf
, чтениеstaticCompositionLocalOf
не отслеживается Compose. Изменение значения приводит к перекомпоновке всей лямбдыcontent
, в которой указанCompositionLocal
, а не только тех мест, гдеcurrent
значение считывается в композиции.
Если значение, предоставленное CompositionLocal
вряд ли изменится или никогда не изменится, используйте staticCompositionLocalOf
чтобы получить преимущества в производительности.
Например, система дизайна приложения может быть продумана так же, как компонуемые элементы повышаются с использованием тени для компонента пользовательского интерфейса. Поскольку различные уровни доступа приложения должны распространяться по всему дереву пользовательского интерфейса, мы используем 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
Компонуемый 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 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 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 fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
Рекомендуется для вас
- Примечание. Текст ссылки отображается, когда JavaScript отключен.
- Анатомия темы в Compose
- Использование представлений в Compose
- Котлин для Jetpack Compose