CompositionLocal — это инструмент для неявной передачи данных через композицию. На этой странице вы узнаете подробнее, что такое 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 , что позволяет получить к ним доступ позже в любой дочерней части композиции. В частности, это свойства LocalColorScheme , LocalShapes и LocalTypography , к которым можно получить доступ через атрибуты colorScheme , shapes и typography темы 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 ) }
Экземпляр 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 использовались внутри Material Composable. Для доступа к текущему значению 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
Для создания CompositionLocal существует два API:
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
- Kotlin для Jetpack Compose