Jetpack Compose — это набор инструментов для построения современных UI (пользовательских интерфейсов) в Android?приложениях. Компания Google анонсировала Jetpack Compose в 2019 году, а уже в марте 2021 года выкатила бета?версию фреймворка.
Стабильный Jetpack Compose 1.0, который мы ждали почти 2 года, вышел 28 июля 2021 года. К этому моменту мы в GO Digital уже были подкованы в знаниях библиотеки и активно тестировали разработку UI с помощью фреймворка (без XML?файлов) в текущих проектах. Об одном из таких кейсов расскажем ниже.
Наш клиент — «Бланк», банк для предпринимателей. В его официальном приложении на Android мы работали над пользовательским интерфейсом чата поддержки. Конечная цель — сделать удобный и отзывчивый UI.
Android Studio,
Зона ответственности в рамках проекта – первые два пункта – сообщения и файлы. Задача – интегрировать Chatwoot с приложением «Бланка» с помощью Jetpack Compose.
Chatwoot – это пакет поддержки клиентов с открытым исходным кодом. Он работает через WebSocket и REST API. Благодаря Chatwoot саппорт-менеджеры могут просматривать чаты из разных каналов на одной панели.
Jetpack Compose – мощная библиотека с современным и интуитивно понятным подходом к созданию UI. С ее помощью мы создали бесшовный и удобный сервис поддержки для клиентов «Бланка» на Android. Подробнее про Jetpack Compose читайте в нашем обзоре.
В разработке UI чата мы использовали MVI-паттерн.
MVI-паттерн (Model-View-Intent) – это архитектурный подход к разработке мобильных приложений, который упорядочивает процесс взаимодействия между пользовательским интерфейсом (View), бизнес-логикой (Model) и пользовательскими действиями (Intent). Он отличается от других архитектурных подходов тем, что фокусируется на пользовательском взаимодействии, а не на состоянии приложения.
В данном коде определены три интерфейса: ViewNews, ViewState и ViewWish.
Класс BaseViewModel() наследует класс ViewModel и определяет общий шаблон (т. е. абстрагирует) для всех ViewModel, которые используют MVI?паттерн.
Внутри этого класса есть защищенный метод subscribeToWish(). Он подписывается на MutableSharedFlow событий типа ViewWish, т. е. на поток событий, которые UI хочет получить или выполнить. Как только такое событие поступает в поток, оно обрабатывается методом handleWish(), определяющимся в классе?наследнике BaseViewModel.
Объявление val wish: SharedFlow = _wish позволяет получать доступ к MutableSharedFlow извне в качестве Immutable SharedFlow, т. е. только для чтения.
Такой подход к обработке интентов пользователя, позволяет создавать отзывчивый и масштабируемый интерфейс, который быстро реагирует на действия пользователя и обеспечивает согласованность состояния приложения.
В Jetpack Compose нет прямой поддержки анимации при первом показе в листах, например, LazyColumn, LazyRow.
Например, у нас есть следующий код для анимации:
Он создает виджет AnimatedVisibility, который принимает несколько параметров:
В конечном итоге AnimatedVisibility обеспечивает плавный переход между состояниями видимости элемента с помощью анимаций, заданных в параметрах enter и exit. В нашем примере AnimatedVisibility показывает содержимое элемента UI, когда visibleState устанавливается в true (при запуске анимации).
Мы столкнулись с такой проблемой: ранее просмотренные (и, следовательно, проанимированные) сообщения, повторно анимируются при возврате к ним. Это смотрится некорректно.
Решение: применили AnimatedHelper.
AnimatedHelper – вспомогательный класс, который определяет, какие элементы списка уже анимированы, а какие – нет. Это подходит для нашей ситуации с возвратом к просмотренным сообщениям.
AnimatedHelper содержит HashMap, хранящий информацию о видимости и анимации для каждого сообщения. Ключом в HashMap выступает Any? – объект, который можно использовать для идентификации каждого сообщения. Значение Boolean указывает, должно ли сообщение быть видимым и проанимированным.
Метод isAnimated принимает ключ key и возвращает Boolean, указывающий, было ли сообщение уже проанимировано. Если ключа нет в HashMap, метод вернет значение false.
Метод setAnimatedValue устанавливает значение value для ключа key в HashMap. Это позволяет изменить значение видимости и анимации для каждого сообщения.
Итак, при возврате к ранее видимым сообщениям AnimatedHelper используется для проверки, должно ли сообщение быть проанимировано или нет. Если значение isAnimated для ключа равно false, значит, сообщение еще не было проанимировано и нужно запустить анимацию. Если значение isAnimated равно true, значит сообщение уже было проанимировано и анимация не нужна.
Применение AnimatedHelper – хорошее временное, но неоптимальное решение. Разработчики Jetpack Compose уже знают о проблеме и работают над ее исправлением. Ожидаем, что в будущих версиях Jetpack Compose появится функционал для управления анимациями в листах при первом и последующих показах. Сейчас у них есть только анимация при смене мест элементов.
Альтернативное решение, которое мы не использовали, – это Android Views и RecyclerView. С их помощью тоже можно решить вопрос с анимациями в листах при первом показе. Но тогда утрачивается концепция применения чистого Jetpack Compose. К тому же технология стара. Compose предлагает новый и более современный подход к созданию UI, повышает производительность и оптимизацию для приложения.
В Jetpack Compose не рекомендуется применять StateFlow для одноразовых событий (News) при MVI-паттерне. Для них в архитектуре MVI используется поток данных SharedFlow, который может иметь несколько «слушателей».
В нашем случае использование collectAsState() для SharedFlow не подходит для передачи News, потому что он предназначен для использования со StateFlow и не уведомляет о новых значениях, если значение потока не изменилось. Вместо этого мы использовали простой collect для SharedFlow в коробке LaunchedEffect, который уведомляет о новых значениях в потоке, когда они появляются.
Если мы используем SharedFlow или любой другой Flow, при каждой рекомпозиции будет создаваться новый Flow и заново запускаться код, который был передан в функцию onEach(). Это может привести к дополнительным нагрузкам на систему.
LaunchedEffect позволяет запустить асинхронную операцию один раз и сохранить ее состояние с ключом между перерисовками. В итоге при каждой рекомпозиции не будет запускаться новая операция, а будет использоваться сохраненное состояние. Это позволит избежать нежелательных эффектов и улучшить производительность приложения.
В результате тестирования обнаружили медленную работу приложения. Чтобы понять, что именно замедляет процессы, обратились к Layout Inspector: проанализировали структуру и свойства View. Если функция передается без адреса (не как ссылка), то при рекомпозиции создается новая лямбда, что может тормозить работу чата.
Решили вопрос с помощью функции remember, которая сохраняет первоначальное лямбда-выражение.
Использование функции remember помогло сохранить состояние между рекомпозициями и оптимизировать работу приложения. При использовании remember создается кэш-значение, которое сохраняется между вызовами функции. При повторных вызовах не происходит повторной рекомпозиции. Это повышает производительность и уменьшает нагрузку на систему. Например, влияет на скорость скролла чата.
Remember также можно применять для кэширования результатов дорогостоящих вычислений, чтобы избежать повторного вычисления при рекомпозиции. Это особенно полезно при работе с большими данными или сложными расчетами.
Remember – не универсальное решение для всех случаев оптимизации в Jetpack Compose. Иногда может потребоваться использовать другие методы оптимизации, например, Memoization внутри Composable функций. Каждый случай требует индивидуального подхода.
Работа над интерфейсом чата для «Бланка» заняла у нас полтора месяца. Несмотря на описанные нюансы, использовать Jetpack Compose было удачным решением. Тем более мы давно пишем на Kotlin. Для нашей команды Compose явно проще, чем XML-макеты. Композитные функции удобно переиспользовать в разных частях программного продукта. Компактный код, доступность сочетания фреймворка с другими библиотеками, легкость внесения правок без изменения структуры – причины, по которым мы рекомендуем разработчикам познакомиться с Jetpack Compose.