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

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()
    }
}

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