Пост

Jetpack Compose. Введение

В современной разработке мобильных приложений пользовательский интерфейс играет одну из ведущих ролей, определяя как восприятие приложения пользователем, так и его взаимодействие с ним. С появлением Jetpack Compose, декларативного фреймворка для Android UI от Google, разработчики получили инструмент, который существенно ускоряет создание интерфейса. В Compose некоторые частовстречающие кейсы реализовать гораздо легче. Например, анимации, списки, сложные компоновки, которые ранее делались через LayoutManager’ы.

Jetpack Compose предлагает полностью новую архитектуру для построения пользовательских интерфейсов на Android, делая ставку на компонентный подход и реактивное обновление данных. Кроме этого, имеется прямая интеграция с корутинами, что значительно облегчает реализацию определенных кейсов. Например, реализацию таймера. За кулисами Compose использует ряд инновационных решений и оптимизаций, благодаря чему достигается высокая производительность и эффективность. Однако для полноценной работы с этим инструментом важно понимать не только основы его использования, но и то, как именно Compose функционирует “под капотом”.

Compose написан на Kotlin и активно использует парадигму функционального программирования, что делает достаточно увлекательным изучение внутреннего кода фреймворка.

Начнем изучение с описания процесса рендеринга, далее рассмотрим принципы работы Compose, подходы к перерисовке UI элементов, методам оптимизации.

Рендеринг в Jetpack Compose - это комплексный процесс, который начинается с описания интерфейса пользователя через декларативные компоненты (composables) и заканчивается отображением этих компонентов на экране. Чтобы понять этот процесс, давайте последовательно рассмотрим его ключевые шаги:

1. Описание UI через Composables

Все начинается с описания пользовательского интерфейса через декларативные функции в Kotlin, называемые composables. Разработчики определяют, какие компоненты должны быть на экране и как они должны вести себя в зависимости от состояния приложения. Это описание не привязано к конкретной реализации отображения, а скорее задает высокоуровневые требования к UI.

2. Превращение Composables в UI Tree

Когда происходит вызов функций composables, Jetpack Compose строит или обновляет UI tree - структуру данных, которая визуально представляет иерархию интерфейса. Этот этап важен, поскольку UI tree используется для вычисления различий между предыдущим и текущим состоянием интерфейса.

3. Slot Table как запись состояний UI

Параллельно с UI tree строится и обновляется слот-таблица. Это особая структура данных, которая хранит текущее состояние каждого composables, отображаемого на экране. Слот-таблица позволяет Compose быстро доступать к состояниям элементов интерфейса и определять, какие именно части UI нуждаются в перерисовке.

4. Рекомпозиция

Перерисовка (или рекомпозиция) в Jetpack Compose начинается, когда изменяется состояние, влияющее на отображение компонентов. Compose сравнивает новое состояние интерфейса с предыдущим, используя слот-таблицу и UI tree, чтобы определить, какие composables изменились и требуют обновления.

5. Layout process

После того как определены элементы для рекомпозиции, Compose определяет размер и положение этих элементов на экране - это называется layout process. Каждый элемент рассматривает ограничения (constraints) своего родителя и определяет собственные размеры в соответствии с ними.

6. Отрисовка

На последнем этапе, когда известны размеры и положение всех элементов, Compose отрисовывает их на экране. Это включает в себя рисование текста, изображений, фонов, анимаций и т.д. Рисование происходит с использованием низкоуровневых API, таких как Canvas.

Далее немного детальнее рассмотрим некоторые шаги:

Что такое рекомпозиция?

Рекомпозиция в Jetpack Compose - это процесс, при котором виджеты (или Composables) перестраиваются в ответ на изменения в данных, которые они отображают. Это основа для реактивного обновления интерфейса, позволяя приложению быть отзывчивым и динамичным. Рекомпозиция инициируется асинхронно и оптимизирована для высокой производительности.

Как Jetpack Compose решает задачу рекомпозиции?

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

Observer pattern и скопированные состояния

Jetpack Compose использует шаблон наблюдателя (Observer pattern) для отслеживания изменений в состоянии. Когда состояние изменяется, система отмечает соответствующие Composables для рекомпозиции. Это достигается за счет использования mutableStateOf, что создает реактивное состояние, которое автоматически оповещает подписчиков о любых изменениях.

Рекурсивная рекомпозиция

Процесс рекомпозиции в Jetpack Compose рекурсивен. Компоненты, отмеченные для рекомпозиции, в свою очередь могут отмечать для рекомпозиции дочерние компоненты, создавая каскад обновлений, который распространяется вниз по дереву компонентов.

Минимизация работы

Алгоритмы Jetpack Compose направлены на минимизацию необходимой работы при рекомпозиции. Для этого используются различные стратегии, такие как интеллектуальное кэширование и отложенная рекомпозиция компонентов, которые могут не быть видимы пользователю сразу.

Процесс рендеринга

После того, как компоненты отмечены для рекомпозиции и обновлены, начинается процесс рендеринга. Jetpack Compose переносит обновленное состояние компонентов на экран устройства.

UI tree и Slot table

При рендеринге, Jetpack Compose использует две ключевые структуры данных: UI tree и Slot table.

UI tree - представляет собой дерево всех UI компонентов (composables) приложения. Узлы дерева соответствуют компонентам интерфейса пользователя, начиная от корневого элемента и заканчивая самыми мелкими визуальными элементами. UI tree динамично изменяется во время выполнения приложения в соответствии с изменениями в состоянии данных, пользовательскими взаимодействиями и другими событиями.

Каждый узел в UI tree соответствует конкретному composables и содержит в себе всю необходимую информацию для рендеринга: стили, текст, изображения и т.д. Jetpack Compose использует UI tree для определения того, как и в каком порядке нужно отрисовывать элементы интерфейса.

Slot table - это структура данных, используемая в Jetpack Compose для хранения состояния компонентов пользовательского интерфейса и их иерархии. Она представляет собой линейную структуру, которая позволяет Compose оптимизировать процесс рекомпозиции, реализуя быстрый поиск и обновление состояния компонентов.

Slot table тесно связана с UI tree, однако отличается способом хранения данных. Вместо того, чтобы хранить информацию в иерархической структуре, как это делается в UI tree, Slot table организует данные линейно. Это значительно ускоряет процесс рекомпозиции, так как любое изменение в данных компонента может быть быстро отражено в интерфейсе пользователя без необходимости полного перестроения иерархии дерева.

Особенности:

  1. Инкрементная рекомпозиция: Slot table позволяет фреймворку выполнять инкрементную рекомпозицию, т.е. обновлять только те части интерфейса, которые действительно изменились, что значительно повышает производительность приложения

  2. Управление состоянием: с помощью Slot table, Compose обеспечивает более эффективное управление состоянием компонентов. Разработчик может изменять состояние в одном месте, и эти изменения будут автоматически отражены во всем интерфейсе, без дополнительного кода или сложных манипуляций с данными

  3. Оптимизация производительности: использование Slot table для хранения состояния и иерархии компонентов позволяет Compose минимизировать количество работы, необходимое для рекомпозиции и рендеринга интерфейса, что делает приложения созданные более быстрыми и отзывчивыми.

Умный рендеринг

Jetpack Compose оптимизирует процесс рендеринга, используя концепцию “умного рендеринга”, который определяет самый эффективный способ отрисовки изменений на экране. Разработчикам не нужно беспокоиться о явном определении перерисовки экрана; Compose самостоятельно решает, как лучше всего отобразить изменения в UI.

Заключение

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

Далее рассмотрим, как мы, разработчики, можем помочь Compose уменьшить количество рекомпозиций, минимизировать излишние вычисления и в целом заставить фреймворк работать оптимально.

P.S. В процессе чтения AOSP (Android Open Source Project) были найдены некоторые любопытные решения, которые можно применять у себя на практике:

  • Расширения для списков. Например, fastForEach с произвольным доступом на 10 миллионах элементов у меня отрабатывал до 50% быстрее.
  • SlotTable. Непосредственно сама структура данных SlotTable
  • Структура SessionMutex. Думаю, попробовать использовать такой подход с сессиями в рабочем проекте для создания пользовательских сессий. Например, флоу авторизации:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@JvmInline
value class SessionMutex<T> private constructor(
    private val currentSessionHolder: AtomicReference<Session<T>?>
) {
    constructor() : this(AtomicReference(null))
  
    /**
     * Returns the current session object.
     */
    val currentSession: T?
        get() = currentSessionHolder.get()?.value
  
    /**
     * Cancels any existing session and then calls [session].
     * [session] will in turn be cancelled if this method is called again before it returns.
     *
     * @param sessionInitializer Called immediately to create the new session object, before
     * cancelling the previous session. Receives a [CoroutineScope] that has the same context as
     * [session] will get.
     * @param session Called with the return value from [sessionInitializer] after cancelling the
     * previous session.
     */
    suspend fun <R> withSessionCancellingPrevious(
        sessionInitializer: (CoroutineScope) -> T,
        session: suspend (data: T) -> R
    ): R = coroutineScope {
        val newSession = Session(
            job = coroutineContext.job,
            value = sessionInitializer(this)
        )
        currentSessionHolder.getAndSet(newSession)?.job?.cancelAndJoin()
        try {
            return@coroutineScope session(newSession.value)
        } finally {
            // If this session is being interrupted by another session, the holder will already have
            // been changed, so this will fail. If the session is getting cancelled externally or
            // from within, this will ensure we release the session while no session is active.
            currentSessionHolder.compareAndSet(newSession, null)
        }
    }
    private class Session<T>(val job: Job, val value: T)
}
Авторский пост защищен лицензией CC BY 4.0 .

© Marche1os. Некоторые права защищены.

Использует тему Chirpy для Jekyll

Популярные теги