Данные локального масштаба с помощью CompositionLocal

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.
Рисунок 1. Предварительный просмотр составного объекта 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()
    }
}

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}