Composable функции
Знакомство с Jetpack Compose лучше всего начать с Composable функций. Они являются кирпичиками нашего UI и хранятся в древовидной структуре данных. Для того чтобы работать с Jetpack Compose, нужно сформировать Compose mindset.
Для того чтобы создать нашу первую Composable функцию, нам нужно добавить аннотацию @Composable. Таким образом любая функция в Kotlin может превратиться в Composable - функцию.
@Composable объявленная над Kotlin функцией говорит нашему Compose компилятору - конвертируй эту ноду в UI ноду и зарегистрируй в дереве функций. Стоит учесть момент, что Composable функция ничего не возвращает. Она является Unit функцией. Она получает входные данные и производит отрисовку UI. Ее работа очень похожа на некий side-эффект.
На языке Compose это называется emitting. Composable функция производит отрисовку во время этапа композиции.
Главной целью для которой служат такие функции является обновление дерева лежащего в памяти. Это позволяет поддерживать актуальное состояние UI. Функции реагируют на изменения данных и перезапускаются параллельно с ним. Также Composable функции могут читать или записывать состояние из дерева.
Аннотирование функции @Composable аннотацией изменяет тип данной функции. Придает ей новые свойства. Разблокирует возможности библиотеки Compose.
Compose runtime ожидает на вход Composable функции. Применяет к ним различные оптимизации.
- parallel composition - параллельная композиция
- smart recomposition - умная рекомпозиция
- positional memoization - позиционная мемоизация
Большинство свойств Compose включены компилятором. C того момента как он стал плагином для Kotlin, он проходит все фазы компиляции и имеет доступ ко всей информации, к которой имеет доступ Kotlin компилятор. Это свойство позволяет перехватывать и трансформировать IR (intermediate representation) всех Composable функций и добавлять к ним дополнительную информацию.
Одной из главных модификаций является добавление параметра Composer как последнего параметра к каждой функции. Его инстанс инжектится в функции в рантайме и прокидывается дочерним функциям в дереве.
Как мы можем видеть Composer прокинут во всех вызовы в пределах дерева. У Compose есть строгое правило
Composable функции могут вызываться только из других Composable функций
Это называется calling context required. И подразумевает тот факт что дерево будет построено только из Composable функций. Так параметр Composer может быть передан ниже по дереву.
Composer является посредником между кодом Compose который мы пишем и компилятором. Composable функции используют его для того, чтобы сообщать о своих изменениях, создавать in-memory представление, а также обновлять состояние.
Composable функции ожидаемо должны быть идемпотентны относительно ноды дерева где они произведены. Перезапуск Composable функции с одними и теми же параметрами много раз должен приводить к тому, что результат будет храниться в том же месте в дереве. Jetpack Compose runtime полагается на такие механизмы как рекомпозиция.
В Jetpack Compose рекомпозиция является действием в ответ на повторный запуск функции. В момент ее выполнения могут произойти изменения в дереве функций. Runtime должен иметь возможность пересобирать функции время от времени при изменении их параметров по разным причинам.
Процесс рекомпозиции проходит вниз по дереву компонентов проверяя какие из них подлежат изменению. Пропуск ноды дерева возможен в том случае если представленная Composable функция является той же самой и ее перезапуск приведет к тому же самому результату.
Отсутствие бесконтрольных сайд - эффектов
Сайд - эффект - это любое действие которое происходит без контроля во время вызова функции. Чтение локальных файлов, походы в сеть и кэш или установка глобальной переменной рассматриваются как сайд - эффекты. Они делают вызываемую функцию зависимой от внешних факторов которые могут иметь влияние на ее поведение.
Сайд - эффекты в контексте Compose это плохо. Composable функция должна быть чистой функцией. Compose ожидает предсказуемость от функций. Таким образом они могут быть многократно перезапущены. Многократный запуск функции дающий разные результаты повлияет на актуальность состояния производимого функцией.
Другой распространенный пример - зависимость Composable функций друг от друга. Такое поведение должно быть исключено!
Для работы с сайд эффектами в контролируемой среде в Jetpack Compose есть Effect Handlers (обработчики сайд-эффектов). Они позволяют делать асинхронные операции с привязкой к жизненному циклу функций.
Ранее было упомянуто, что Composable функции это не стандартные функции, которые могут быть вызваны лишь разово и вызывать из себя еще функции. Composable функции могут перезапускаться во время рекомпозиции.
Compose выборочно решает какой ноде нужно выполниться. Эти функции спроектированы для многократного вызова и обновления состояния.
Мемоизация (Positional memoization)
Positional Memoisation - это возможность функции закэшировать ее результат основываясь на входных данных. Таким образом у нас отпадает необходимость повторного вызова функции.
В функциональной мемоизации вызов функции будет идентифицирован через комбинацию ее параметров имени, типа, значений. Уникальный ключ для такой функции будет сгенерирован на основе ее сигнатуры. В Compose мемоизация основывается на их местоположении. В runtime будут сгенерированы разные id (уникальные в пределах родителя), когда одна и та же функция с одними и теми же параметрами вызывается из разных мест.
Идентификатор сохранется во время рекомпозиции. Рантайм таким образом может узнавать, была ли вызвана функция ранее и нужно ли ее пропустить в момент рекомпозиции. Иногда сохранять уникальные идентификаторы бывает проблематично. К примеру возьмем случай, когда мы рисуем список в цикле.
В этом примере функция Talk(talk) вызывается в одном и том же месте, но представляет разные элементы списка и как следствие разные ноды в дереве. В подобном случае Compose Runtime полагается на порядок вызовов и генерирует идентификаторы с его учетом. Это работает хорошо, при добавлении элемента в конец списка, когда остальные элементы остаются на тех же позициях что и прежде. Но, что произойдет если мы добавим элементы наверх или в середину списка? Элементы списка рекомпозируются, так как их позиции будут смещены, даже если их содержимое не изменилось. Это крайне неэффективно в примерах, где мы работаем с большими списками. Для решения этой проблемы Compose предлагает использовать ключи. Мы можем явно задать ключ элементу списка
В приведенном выше примере мы используем Talk.id как уникальный для каждого элемента списка идентификатор. Таким образом мы сохраняем идентичность элементов независимо от их позиции.
Также часто разработчики хотят оперировать какими - то тяжело вычисляемыми параметрами в рамках Composable функций. Другими словами, мы хотим сохранять результат тяжело вычисляемых операций. Для этого Compose Runtime предоставляет нам remember функцию.
Функция remember - это Composable функция, которая знает как читать и записывать что - то в in-memory структуру, хранящую состояние дерева. Она использует механизм позиционной мемоизации. Мемоизация работает не на глобальном уровне приложения в общем, а в рамках жизненного цикла Composable функции
В Kotlin приостанавливаемые функции могут вызываться только из других приостанавливаемых функций. Они также полагаются на контекст вызова (calling context).
Это гарантирует что suspend функции могут быть связаны в цепочку друг с другом и дает Koltin компилятору возможность прокинуть или заинжектить что - то через все уровни вычисления. Параметр Continuation передается в конце последним параметром функции и обеспечивает доступ к продвинутым функциям языка Kotlin.
Continuation несет в себе информацию, которая необходима рантайму Kotlin’a для запуска и приостановки данной функции. Такими же свойствами наделяет функцию аннотация @Composable. Она делает из стандартной Kotlin функции перезапускаемую, реактивную и тд.. функцию.
Дополнительно
Composable функции имеют различные ограничения, возможности в отличии от стандартных функций. Это все можно представить как форму function coloring. Так как они представляют разные категории функций.
function coloring - концепт представленный Бобом Нилстромом из команды Dart в Google.
Боб делит разные функции на категории. suspend функции тоже маркируются определенным образом. У нас есть возможность вызывать одну suspend функцию из другой. Совмещение стандартного программирования с асинхронным требует наличие механизмов интеграции (coroutine launch points).
Jetpack Compose тоже нуждается в таких точках интеграции. Этим может выступать функция setContent { }.
Как достигается возможность function coloring? Как показано на рисунке выше мы вызываем обычную функцию forEach из Column { } которая является @Composable функцией. Это возможно благодаря inline! Операторы коллекций объявлены как inline.
Аннотация Composable меняет тип функции в compile time. C точки зрения синтаксиса тип Composable функции это @Composable (T) -> A, где А может быть Unit или любым другим типом, если функция возвращает значение. Разработчики используют эту возможность чтобы объявлять лямбды Composable, также как они это делают в Kotlin.
У таких функций есть свой скоуп @Composable Scope.() -> A часто используемый для компоновки внутренней информации функций которые также являются Composable. Компилятор проводит статическую валидацию данных, определяет порядок вызова функций и многое другое.