Используйте CompositionLocalProvider, чтобы перенести шаблон Provider во вселенную Jetpack Compose.

Введение

После смены парадигмы в сторону Jetpack Compose как надежной среды пользовательского интерфейса, разработка современных приложений для Android резко возросла. Новая декларативная и интуитивно понятная структура пользовательского интерфейса помогает быстро создавать приложения для Android с повышенной производительностью.

Jetpack Compose фактически имеет древовидную структуру (такую ​​же, как, например, во Flutter), где мы указываем все узлы для рендеринга, и на основе предоставленных свойств элементы отрисовываются на экране.

Мы знаем, что свойства или состояние передаются по дереву вниз до его последнего узла, а генерируемые события распространяются вверх для обработки. Этот шаблон иногда становится утомительным, когда приходится передавать определенный объект, созданный только на более высоком узле, вниз по дереву к целевому узлу.

В этой статье мы обсудим проблему и то, как Jetpack Compose предлагает элегантное решение.

Предпосылки

Здесь мы затронули расширенную концепцию Jetpack Compose, а также несколько концепций «Внедрение зависимостей с использованием Hilt». Базовые знания о них лучше иметь при чтении этой статьи.

Теория

Рассмотрим следующий вариант использования в качестве дерева Compose UI:

Корневой узел создает объект A, который используется дочерним элементом 4-го уровня, Child 4. A не является одноэлементным, и поскольку его экземпляр не может быть создан откуда-либо, кроме корневого узла, созданный экземпляр должен быть передан вниз по иерархии. дерево до дочернего элемента 4. В этом случае экземпляр A без необходимости удерживается дочерними элементами с 1 по 3, что бесполезно для этих узлов. Мы хотим каким-то образом получить доступ к A напрямую из Child 4:

Во Flutter эту проблему можно решить с помощью шаблона Provider, где элемент может быть предоставлен глобально в дереве, а затем к нему можно получить доступ из любого места. Посмотрим, как этого можно добиться в Jetpack Compose.

Введите «CompositionLocalProvider»

CompositionLocalProvider может предоставить ссылку на какой-либо объект с более высокого уровня в дереве, и любой дочерний элемент может получить прямой доступ к предоставленному значению.

Это использует CompositionLocal внутри, которое относится к поддереву. Если мы поместим его на корневой уровень дерева пользовательского интерфейса, он покроет все дерево под ним, таким образом действуя как глобальный поставщик.

Говори код, чувак!

Пример использования

Типичный вариант использования, который мы рассмотрели здесь, — регистрация события Firebase Analytics из составной функции по нажатию кнопки.

@Singleton
class AnalyticsManager @Inject constructor() {
private val firebaseAnalytics: FirebaseAnalytics by lazy {
Firebase.analytics
}
fun logEvent(eventName: String) {
firebaseAnalytics.logEvent(eventName, null)
}
fun logEventWithParams(eventName: String, params: Bundle) {
firebaseAnalytics.logEvent(eventName, params)
}
}

Мы создали класс AnalyticsManager с помощью Hilt в качестве абстракции для регистрации различных аналитических событий в нашем приложении. В ViewModel classes это можно внедрить напрямую, но когда речь идет о составных функциях, мы не можем напрямую внедрить экземпляр этого класса в параметры функции.

Объявление CompositionLocal

В тех случаях, когда щелчок по кнопке напрямую вызывает регистрацию любого события аналитики (т. е. без передачи этого события для обработки ViewModel), нам необходимо предоставить экземпляр AnalyticsManager. Теперь вместо того, чтобы передавать этот экземпляр вниз по дереву составному объекту, имеющему такую ​​кнопку, давайте посмотрим, как мы использовали CompositionLocalProvider для предоставления необходимого экземпляра:

val LocalAnalytics = staticCompositionLocalOf<AnalyticsManager?> { null }
@Singleton
class AnalyticsManager @Inject constructor() {
// Body omitted for brevity
}

На глобальном (статическом) уровне мы объявили создателя CompositionLocal для нашего класса AnalyticsManager с именем LocalAnalytics. Это достигается путем объявления StaticProvidableCompositionLocal с использованием функции staticCompositionLocalOf<T> и предоставления начального значения по умолчанию. Это создаст статический экземпляр CompositionLocal, который затем можно будет предоставить соответствующим частям дерева пользовательского интерфейса.

ℹ️Обратите внимание, поскольку мы не можем указать значение по умолчанию для нашего класса AnalyticsManager с поддержкой Hilt, мы установили его равным нулю. В этом случае это можно решить, потому что позже мы установим правильное ненулевое значение. Но всегда лучше указать ненулевое начальное значение по умолчанию.

ℹ️Мы можем использовать другую функцию compositionLocalOf<T> вместо staticCompositionLocalOf<T> . Они отличаются тем, как дерево повторно отображается в зависимости от изменения предоставленного значения. Подробнее об этом читайте в официальной документации Android здесь.

Предоставление значения для CompositionLocal

У нас есть один основной ComponentActivity в нашем коде, который управляет функциями Composable, обрабатывает навигацию и прочее, куда мы можем внедрить начальный экземпляр класса AnalyticsManager, который затем может быть предоставлен.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var analyticsManager: AnalyticsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalAnalytics provides analyticsManager) {
// Further UI tree
}
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Мы использовали CompositionLocalProvider для предоставления инжектированного экземпляра класса AnalyticsManager. Этот экземпляр заменит начальное «нулевое» значение и предоставит действительное значение. Обратите внимание, что мы объявили его перед объявлением нашего дерева пользовательского интерфейса, поэтому предоставленный экземпляр доступен для всего приложения. В этом случае мы использовали функцию provides; мы можем добавить еще много таких положений, чтобы обеспечить несколько объектов по всему миру!

Потребитель Провайдера

В потребительском конце мы можем использовать предоставленное значение как:

@Composable
fun ButtonComposable() {
val analyticsManager = LocalAnalytics.current
Button(
onClick = {
analyticsManager?.logEvent("button_clicked")
}
) {
Text(text = "Click me!")
}
}

LocalAnalytics было статически объявленным свойством, и мы можем получить доступ к его последнему экземпляру, используя LocalAnalytics.current. Поскольку мы объявили AnalyticsManager как обнуляемый, у нас есть нулевой безопасный доступ, но это может быть верно не для всех случаев. Обратите внимание, что мы не передавали никаких аргументов вниз по дереву, все они предоставляются глобально!

Что делать, если нам нужно изменить предоставленное значение?

Поскольку мы объявили наш CompositionLocalProvider на корневом уровне дерева пользовательского интерфейса, все дерево получает предоставленный экземпляр. Но мы можем изменить это значение, заключив поддерево в новый CompositionLocalProvider. Таким образом, следующее поддерево будет использовать самое последнее значение, а более раннее значение будет переопределено. Это относится только к обернутому поддереву; остальные узлы будут продолжать получать значение, предоставленное на корневом уровне:

Используя CompositionLocalProvider, мы перенесли шаблон Provider во вселенную Jetpack Compose. Подробную информацию о любой специальной функции или модуле вы всегда можете найти в официальной документации CompositionLocal.