Всем привет! Недавно я начала изучать Jetpack Compose. Всё, что я изучаю по иностранным гайдам, я обычно перевожу, чтобы при повторном прочтении, мозг снова не тратил время на перевод. Мне кажется, этот фреймворк становится всё более популярен, поэтому хочу поделиться своим переводом Thinking in Compose с другими начинающими :) Jetpack Compose — это современный декларативный UI Toolkit для Android, упрощающий написание и поддержку UI (пользовательского интерфейса) вашего приложения, и в этом гайде рассказывается за счёт чего это достигается. Исторически иерархия Android вьюшек представлялась в виде дерева UI виджетов. По мере изменения состояния приложения, например, в результате взаимодействия с пользователем, UI иерархия должна обновляться для отображения текущих данных. Наиболее распространенным способом обновления UI является обход дерева с помощью функции findViewById() и изменение его узлов путем вызова таких методов, как button.setText(String), container.addChild(View) или img.setImageBitmap(Bitmap). Эти методы изменяют внутреннее состояние виджета.  Манипулирование вьюшками вручную увеличивает вероятность ошибок. Если часть данных отображается в нескольких местах, то легко забыть обновить одну из вью, в которой данные отображаются. Также легко создать illegal-состояния, когда два обновления вступают в неожиданный конфликт. Например, обновление может попытаться установить значение узла, который только что был удален из UI. В общем, сложность сопровождения программного обеспечения растет с увеличением числа вьюшек, требующих обновления. За последние несколько лет индустрия в целом начала переходить на декларативную UI модель, которая значительно упрощает инженерную работу, связанную с созданием и обновлением UI. Эта методика основана на концептуальном восстановлении всего экрана с нуля и последующем применении только необходимых изменений. Такой подход позволяет избежать сложностей, связанных с ручным обновлением иерархии вьюшек с сохранением их состояния. Compose — это декларативный UI фреймворк. Одна из проблем с восстановлением всего экрана заключается в том, что это потенциально дорого с точки зрения затрачиваемого времени, вычислительной мощности и использования батареи. Чтобы снизить эти затраты, Compose интеллектуально выбирает, какие части UI необходимо перерисовать в каждый момент времени. То как это влияет на создание вами UI элементов, рассматривается в разделе «Рекомпозиция». Используя Compose, вы можете создать свой пользовательский интерфейс, определяя набор composable-функций, которые принимают данные и создают UI-элементы. Вот простой пример виджета Greeting, который принимает String и создает виджет Text, отображающий приветственное сообщение.   Несколько примечательных особенностей этой функции: Функция помечена аннотацией @Composable. Все composable-функции должны быть помечены этой аннотацией, она сообщает компилятору Compose, что функция предназначена для преобразования данных в пользовательский интерфейс.  Функция принимает данные. Composable-функции могут принимать параметры, с помощью которых логика приложения может описать пользовательский интерфейс. В данном случае наш виджет принимает String, чтобы поприветствовать пользователя по имени. Функция отображает текст в UI. Для этого она вызывает composable-функцию Text(), которая, фактически, и создает текстовый UI-элемент. Composable-функции создают иерархию пользовательского интерфейса, вызывая другие composable-функции. Функция ничего не возвращает. Compose-функции, которые создают пользовательский интерфейс, не должны ничего возвращать, поскольку они описывают желаемое состояние экрана, а не создают UI-виджеты. Эта функция быстрая, идемпотентная и не имеет side-эффектов (побочных эффектов).  Функция ведет себя одинаково при многократном вызове с одним и тем же аргументом и не использует другие значения, такие как глобальные переменные или результаты вызова функции random(). Функция описывает пользовательский интерфейс без каких-либо side-эффектов, таких как изменение свойств или глобальных переменных.  В разделе «Рекомпозиция» рассматриваются причины, по которым этими свойствами в общем должны обладать все compose-функции.  При использовании многих императивных объектно-ориентированных UI toolkit-ов вы инициализируете UI, создавая экземпляр дерева виджетов, обычно в XML layout файле. Каждый виджет поддерживает свое собственное внутреннее состояние и предоставляет геттеры и сеттеры, которые позволяют логике приложения взаимодействовать с виджетом.  В декларативном подходе Compose виджеты не сохраняют состояние и не предоставляют сеттеры и геттеры. Фактически, виджеты не являются объектами. Обновление UI осуществляется путем вызова одной и той же composable-функции с различными аргументами. Это упрощает предоставление состояния архитектурным шаблонам, таким как ViewModel, как описано в Руководстве по архитектуре приложений. Далее ваши composable-функции отвечают за обновление UI при каждом обновлении наблюдаемых данных. Когда пользователь взаимодействует с UI, UI вызывает такие события, как onClick. Эти события должны уведомить логику приложения, которая в свою очередь может изменить состояние приложения. При изменении состояния снова вызываются composable-функции уже с новыми данными. В результате UI-элементы перерисовываются — этот процесс называется рекомпозицией. Поскольку composable-функции написаны на языке Kotlin, а не на XML, они могут быть такими же динамичными, как и любой другой Kotlin код. Например, вам необходимо создать UI, который приветствует нескольких пользователей:  Эта функция принимает список имен и выводит приветствие для каждого пользователя. Composable-функции могут быть достаточно сложными. Вы можете использовать операторы if, чтобы определить, хотите ли вы отображать определенный UI-элемент. Вы можете использовать циклы. Вы можете вызывать вспомогательные функции. Вы можете использовать всю гибкость базового языка. Эта мощь и гибкость — одно из ключевых преимуществ Jetpack Compose. В императивной UI модели, чтобы изменить виджет, необходимо вызвать сеттер виджета для изменения его внутреннего состояния. А в Compose вы снова вызываете composable-функцию с новыми данными. При этом происходит рекомпозиция функции — виджеты, выдаваемые функцией, если это необходимо, перерисовываются, с новыми данными. Фреймворк Compose может интеллектуально рекомпозировать только те компоненты, которые изменились.  В качестве примера, рассмотрим composable-функцию, которая отображает кнопку: При каждом нажатии кнопки обновляется значение clicks. Compose вызывает лямбду с функцией Text, чтобы показать новое значение; этот процесс называется рекомпозицией. Для других функции, которые не зависят от этого значения, рекомпозиция производиться не будет. Как мы обсуждали ранее, рекомпозиция всего UI-дерева может быть вычислительно затратной, приводить к расходу вычислительной мощности и времени автономной работы. Compose решает эту проблему с помощью интеллектуальной рекомпозиции. Рекомпозиция — это процесс повторного вызова composable-функций при изменении входных данных. Эффективность этого процесса в том, что Compose выполняет рекомпозицию только для тех функций и лямбд, параметры которых изменились, и пропускает остальные. Никогда не полагайтесь на side-эффекты от выполнения composable-функций, поскольку рекомпозиция функции может быть пропущена. В результате чего пользователи могут столкнуться со странным непредсказуемым поведением вашего приложения. Side-эффект — это любое изменение, которое влияет на остальную часть приложения. Например, все эти действия являются опасными side-эффектами: Изменение значения свойства общего объекта; Обновление observable во ViewModel; Обновление SharedPreferences Composable-функции могут выполняться повторно так же часто, как сменяются кадры (frame), например, при воспроизведении анимации. Composable-функции должны быть быстрыми, чтобы избежать зависаний во время анимации. Если вам необходимо выполнить дорогостоящие операции, такие как чтение Shared Preferences, сделайте это в background coroutine и передайте значение результа composable-функции в качестве параметра.   В качестве примера рассмотрим код, создающий composable-функцию для обновления значения в SharedPreferences. Сама composable-функция не должна читать или писать в SharedPreferences. Вместо этого чтение и запись переносятся во ViewModel в background coroutine. Логика приложения передает туда текущее значение и callback для запуска обновления. В этом документе рассматривается ряд моментов, которые следует знать при использовании Compose: Composable-функции могут выполняться в любом порядке. Composable-функции могут выполняться параллельно. Рекомпозиция пропускает как можно больше composable-функций и лямбд. Рекомпозиция оптимистична и может быть отменена. Composable-функция может выполняться достаточно часто, вплоть до каждого кадра анимации. В следующих разделах мы рассмотрим, как писать composable-функции, поддерживающие рекомпозицию. Вкратце, наилучшей практикой является обеспечение быстродействия, идемпотентности и отсутствия side-эффектов у composable функций.  Если посмотреть на код composable-функции, то можно предположить, что код выполняется в том порядке, в котором он написан. Однако это не обязательно так. Если composable-функция содержит вызовы других composable-функций, то эти функции могут выполняться в любом порядке. Compose имеет возможность распознавать, что некоторые UI-элементы имеют более высокий приоритет, чем другие, и отрисовать их в первую очередь.  Например, у вас для отрисовки трех экранов в лайауте вкладок (tab layout) такой код: Вызовы StartScreen, MiddleScreen и EndScreen могут происходить в любом порядке. А это значит, что нельзя, например, в StartScreen() задать значение некоторой глобальной переменной (side-эффект), а MiddleScreen() прочитать значение этой переменной, рассчитывая, что оно было задано в StartScreen(). Каждая из этих функций должна быть автономной. Compose может оптимизировать рекомпозицию за счет параллельного выполнения composable-функций. Это позволяет Compose использовать преимущества нескольких ядер и запускать composable-функции, не находящиеся на экране, с более низким приоритетом.  Такая оптимизация означает, что composable-функция может выполняться в пуле фоновых потоков. Если composable-функция вызывает функцию во ViewModel, то Compose может вызвать эту функцию из нескольких потоков одновременно. Чтобы обеспечить корректное поведение приложения, у всех composable-функций не должно быть side-эффектов. Side-эффекты следует вызывать из callback-ов таких, как onClick, которые всегда выполняются в UI-потоке. Когда вызывается composable-функция, вызов может происходить в другом потоке, отличном от вызывающего. Это означает, что следует избегать кода, который изменяет переменные в composable-лямбде — как потому, что такой код не является потокобезопасным, так и потому, что это недопустимый side-эффект для composable-лямбды. Приведем пример composable-функции, которая выводит список и количество его элементов: Этот код не имеет side-эффектов, он преобразует полученный список в UI. Это отличный код для отображения небольшого списка. Однако если функция будет записывать данные в локальную переменную, то код не будет потокобезопасным и корректным: В этом примере items изменяется при каждой рекомпозиции. Это может быть каждый кадр анимации или обновление списка. В любом случае UI будет отображать неверное значение count. В связи с этим подобные записи не поддерживаются в Compose; запрещая подобные записи, мы позволяем фреймворку менять потоки для выполнения composable-лямбд. Когда части вашего UI невалидны, Compose делает все возможное, чтобы рекомпозировать только те части, которые необходимо обновить. Это означает, что он может пропустить повторный запуск composable-функции одной кнопки без выполнения каких-либо composable-функции выше или ниже её по UI-дереву.  Каждая composable-функция или лямбда может рекомпозироваться отдельно сама по себе. Приведем пример, демонстрирующий, как рекомпозиция может пропускать некоторые элементы при выводе списка: Во время рекомпозиции может потребоваться выполнить только единственную из этих частей. Compose может перейти к лямбда-выражению Column, не выполняя ни одного из его родительских элементов, при изменении header. И при выполнении Column Compose может пропустить элементы LazyColumn, если names не изменилось.  Рекомпозиция начинается всякий раз, когда Compose считает, что параметры composable-функции могли измениться. Рекомпозиция оптимистична, то есть Compose ожидает завершения рекомпозиции до того, как параметры снова изменятся. Если параметр изменится до завершения рекомпозиции, Compose может отменить рекомпозицию и перезапустить её с новым параметром.  При отмене рекомпозиции Compose удаляет UI-дерево, созданное в результате рекомпозиции. Если у вас есть side-эффекты, зависящие от отображаемого UI, то они будут применены даже при отмене композиции. Это может привести к несогласованному состоянию приложения. Убедитесь, что все composable-функции и лямбда-выражения идемпотентны и не имеют side-эффектов для обеспечения корректного выполнения оптимистичной  рекомпозиции. В некоторых случаях composable-функция может выполняться для каждого кадра UI анимации. Если функция выполняет дорогостоящие операции, например чтение из памяти устройства, это может привести к зависанию пользовательского интерфейса.  Например, если ваш виджет попытается прочитать настройки устройства, потенциально он может читать эти настройки сотни раз в секунду, что повлечет за собой катастрофические последствия для производительности вашего приложения. Если вашей composable-функции нужны данные, у неё должны быть параметры для получения этих данных. Затем необходимо переместить дорогостоящую работу в другой поток, за пределами composable-функции, и передать данные в Compose с помощью mutableStateOf или LiveData.Исходный код примеров  Android разработчик. Переводчик в сфере IT