Данные локального масштаба с помощью 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 , что позволяет впоследствии извлекать их в любой дочерней части Composition. В частности, это свойства 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 перекомпонует части Composition, считывающие 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 — это инструмент для неявной передачи данных через композицию .

Ещё одним ключевым признаком использования CompositionLocal является ситуация, когда параметр является сквозным, и промежуточные уровни реализации не должны знать о его существовании , поскольку информирование этих промежуточных уровней ограничит полезность компонуемого объекта. Например, запрос разрешений Android осуществляется с помощью CompositionLocal . Компонуемый объект выбора медиа-контента может добавлять новые функции для доступа к защищённому разрешениями контенту на устройстве без изменения его API и без необходимости для вызывающих объектов выбора медиа-контента знать об этом дополнительном контексте, используемом из среды.

Однако CompositionLocal не всегда является лучшим решением. Мы не рекомендуем злоупотреблять CompositionLocal , поскольку у него есть некоторые недостатки:

CompositionLocal затрудняет понимание поведения компонуемых объектов . Поскольку они создают неявные зависимости, вызывающим компонуемым объектам, которые их используют, необходимо убедиться, что значение для каждого CompositionLocal выполнено.

Более того, для этой зависимости может не быть чёткого источника информации, поскольку она может мутировать в любой части Composition. Таким образом, отладка приложения при возникновении проблемы может быть более сложной, поскольку необходимо перемещаться по Composition, чтобы увидеть, где было предоставлено current значение. Такие инструменты, как «Поиск использований» в IDE или «Инспектор макета Compose», предоставляют достаточно информации для решения этой проблемы.

Решение об использовании CompositionLocal

Существуют определенные условия, которые могут сделать CompositionLocal хорошим решением для вашего варианта использования:

У CompositionLocal должно быть подходящее значение по умолчанию . Если значение по умолчанию отсутствует, необходимо гарантировать, что разработчику будет крайне сложно попасть в ситуацию, когда значение для CompositionLocal не указано. Отсутствие значения по умолчанию может привести к проблемам и разочарованию при создании тестов или предварительном просмотре компонуемого объекта, использующего этот CompositionLocal , поскольку он всегда будет требовать явного указания.

Избегайте использования CompositionLocal для концепций, которые не рассматриваются как имеющие область действия в пределах дерева или подиерархии . CompositionLocal имеет смысл, когда его потенциально может использовать любой потомок, а не только некоторые из них.

Если ваш вариант использования не соответствует этим требованиям, перед созданием CompositionLocal ознакомьтесь с разделом «Альтернативные варианты для рассмотрения» .

Примером плохой практики является создание объекта CompositionLocal , содержащего ViewModel конкретного экрана, чтобы все компонуемые элементы на этом экране могли получить ссылку на ViewModel для выполнения некоторой логики. Это плохая практика, поскольку не всем компонуемым элементам ниже конкретного дерева пользовательского интерфейса требуется информация о ViewModel . Хорошей практикой является передача компонуемым элементам только необходимой им информации, следуя шаблону « состояние передается вниз, а события — вверх» . Такой подход сделает ваши компонуемые элементы более удобными для повторного использования и более простыми в тестировании.

Создание CompositionLocal

Существует два API для создания CompositionLocal :

  • compositionLocalOf : Изменение значения, предоставленного во время перекомпоновки, делает недействительным только то содержимое, которое считывает его current значение.

  • staticCompositionLocalOf : В отличие от compositionLocalOf , чтение staticCompositionLocalOf не отслеживается Compose. Изменение значения приводит к перекомпоновке всего content лямбда-выражения, где предоставляется CompositionLocal , а не только тех мест в Composition, где считывается 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()
    }
}

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}