В статье я хотел бы поделиться нашим опытом внедрения Jetpack Compose, мыслями о его преимуществах, а также привести несколько лучших практик, которые помогут вам в его освоении. Надеюсь, эта статья будет полезна тем, кто хочет попробовать Jetpack Compose в своем проекте.
Совместимость
В архитектуре нашего приложения используется Single-Activity подход, поэтому, как описано в документации, нам нужно было всего лишь заменить реализацию метода onCreateView() у фрагмента, чтобы вернуть ComposeView, который описывает UI экрана.
override fun onCreateView(...): View {
return ComposeView(requireContext()).apply {
setContent { /* In Compose world */ }
}
}
Сразу же заметили одну особенность — первый запуск этого экрана стал занимать значительно больше времени (больше одной секунды!). При поиске причин была найдена отличная статья, в которой описаны результаты исследований, показывающие, что отключенная оптимизация R8 и включенная возможность дебага сборки добавляют по 0,5 секунды ко времени первой загрузки экрана с Compose в приложении. Кроме того, первый экран с Compose в приложении грузится примерно в два раза дольше, чем остальные.
Проведенные нами проверки действительно это подтвердили, и использование Compose может иметь большее влияние на производительность, чем кажется на первый взгляд. Это стоит иметь в виду, но это ни в коем случае не причина, чтобы не внедрять Compose в существующий проект.
Про совместимость можно почитать подробнее в официальной документации.
Интеграция с другими библиотеками Jetpack
Внутри @Composable функций можно получить доступ к ViewModel напрямую, используя функцию viewModel() из библиотеки lifecycle-viewmodel-compose (пока в альфе). Современные фреймворки для DI (Koin, Dagger/Hilt) также предоставляют такую возможность. Однако, так как мы не вносим изменения в граф навигации и отображаем наш экран внутри фрагмента (отчасти для того, чтобы изменения были атомарными и не влияли на остальные части приложения), более простым решением показалось продолжить использовать ViewModel, сохраняя ее по-старому — как переменную во фрагменте.
Тут можно почитать официальные рекомендации по интеграции с другими библиотеками.
Material design
Из-за небольшого размера нашего эксперимента сложно выделить что-то особенное на эту тему. Пожалуй, стоит отметить приложение Material Design catalog, которое точно будет полезным, чтобы понять, какие элементы уже реализованы и готовы к использованию в Jetpack Compose.
Отличным источником информации также послужат официальные примеры приложений, которые частично или полностью написаны на Compose, в этом репозитории на GitHub.
Списки
Если вы видели хотя бы пару видео про Compose, то скорее всего уже отметили, насколько удобная и простая в использовании получилась замена для RecyclerView (LazyList):
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(messages) { message -> MessageRow(message) }
}
}
Также поведение списка очень легко кастомизировать: один из элементов нового дизайна — ViewPager c двумя группами кнопок. Однако на момент написания этой статьи в Compose все еще нет официальной версии ViewPager. Решением этой проблемы может послужить отличная библиотека от Chris Banes, в которой он реализован, но его API отмечен как экспериментальный (кстати, библиотека также используется в официальных примерах и, возможно, в будущем станет частью Compose). Это вполне подойдет для pet-проекта или некоторых приложений, но использование альфа-версий библиотек и экспериментальных API может быть не самой лучшей идеей для банковского приложения.
Хорошим решением может стать использование существующего ViewPager с помощью @Composable AndroidApi. В качестве альтернативы можно было бы использовать LazyRow и какой-либо аналог SnapHelper для RecyclerView. В Compose на данный момент такого аналога нет, но реализовать его самостоятельно намного проще, чем кажется: всего лишь нужно передать кастомную реализацию FlingBehavior в LazyRow. Пример такой реализации можно посмотреть здесь.
После синхронизации Row, в котором добавлены кнопки управления, с состоянием LazyRow через LazyListState конечный результат выглядит следующим образом:
Также множество хороших примеров с открытым кодом (и не только по спискам) можно найти здесь.
Анимации
Наверное, это одна из моих любимых возможностей Jetpack Compose, новый API для анимации невероятно прост и удобен в использовании. Можно получить хорошее представление о нем, посмотрев короткое видео с примерами. За пять минут можно научиться использовать анимации, которые подойдут для большинства случаев. Да, анимации стали настолько удобными!
Лучшие практики для @Composable функций
Следуйте общепринятому порядку параметров в функциях: обязательные, модификатор, параметры с значениями по умолчанию, содержание.
Библиотечные функции написаны именно в таком порядке. Давайте разберем, почему порядок важен, на примере функции, которую мы написали для своей реализации CollapsingToolbarLayout:
@Composable
fun CollapsingToolbar(
title: String,
modifier: Modifier = Modifier,
navigationIcon: NavigationIcon? = null,
config: CollapsingToolbarConfig = CollapsingToolbarConfig(),
scrollState: ScrollState = rememberScrollState(),
content: @Composable ColumnScope.() -> Unit,
)
Указание модификатора (Modifier) в начале или после обязательных параметров позволяет не указывать название аргумента в местах вызова:
@Composable
fun MyScreen() {
CollapsingToolbar(
title = "My title",
Modifier.fillMaxSize(),
navigationIcon = NavigationIcon(R.drawable.ic_arrow_right){
/* обработка нажатия */
},
) {
// лямбда выражение с содержимым!
}
}
Указывая параметр с содержанием (content) на последнем месте, мы сможем использовать лямбда-выражения.
Примеры из стандартной библиотеки: любая @Composable функция.
Используйте вспомогательные классы для группировки связанных параметров.
В примере выше используются два таких класса:
class CollapsingToolbarConfig(
val collapsedToolbarHeight: Dp = 56.dp,
val expandedToolbarHeight: Dp = 112.dp,
val collapsedTitleStartPadding: Dp = 72.dp,
val expandedTitleStartPadding: Dp = 20.dp,
val collapsedTitleFontSize: TextUnit = 20.sp,
val expandedTitleFontSize: TextUnit = 32.sp,
)
class NavigationIcon(
@DrawableRes val icon: Int,
val onClick: (() -> Unit)
)
Использование таких вспомогательных классов позволяет сгруппировать параметры нашего тулбара и улучшает чтение (и написание) кода при его создании.
Также стоит обратить внимание на то, что NavigationIcon используется для группировки ссылки на ресурс с иконкой и функцию, которая должна обработать нажатие на нее. Передавать их отдельно не имеет смысла, так как нам не нужна обработка нажатий, если иконки нет.
Примеры из стандартной библиотеки: TextStyle, PaddingValues, SwitchDefaults.
Создавайте две версии @Composable функций: с состоянием и без (stateful and stateless)
Если функция будет хранить какое-либо состояние, хорошо предусмотреть возможность управления этим состоянием извне. Для этого можно использовать state hoisting (в Compose: паттерн, который заключается в «поднятии» состояния, чтобы оно могло быть управляемым в месте вызова). У LazyRow и LazyColumn состояние задано как параметр функции с значением по умолчанию, соответственно, при вызове этой функции мы можем получить его и изменить при необходимости. В нашем примере мы сделали то же самое — внутри функции параметр scrollState передается с помощью Modifier.verticalScroll (scrollState) в Column, в котором расположено содержимое (content). На экране выше нет необходимости управлять этим состоянием, но так как его в любом случае необходимо инициализировать внутри функции (для согласования скролла содержания и размера тулбара), то нет ничего плохого в том, чтобы объявить его в качестве входного параметра. Это, напротив, сделает нашу функцию более гибкой.
Также стоит отметить, что общепринято называть функции, которые служат для хранения состояния, используя префикс “remember”. Например: rememberLazyListState() и rememberCoroutineScope().
Используйте скоуп содержимого в качестве приемника (receiver) для функций, отображающих содержимое (content)
В Compose модификаторы представляют собой функции расширения, и некоторые из них доступны только в скоупе соответствующих @Composable (BoxScope, ColumnScope и т.д.). Например, в ColumnScope модификатор align():
interface ColumnScope {
fun Modifier.align(alignment: Alignment.Horizontal): Modifier
}
Возвращаясь к нашему примеру, входной параметр content объявлен как:
content: @Composable ColumnScope.() -> Unit.
Таким образом в месте вызова мы получаем доступ к скоупу колонки и можем использовать модификаторы, которые без этого недоступны.
CollapsingToolbar(
title = "My title",
) { this: ColumnScope
Text(
text = "some text",
Modifier.align(Alignment.CenterHorizontally)
)
}
Примеры из стандартной библиотеки: любая @Composable функция с содержимым.
Избегайте дублирования кода.
В Compose акроним DRY (don’t repeat yourself) стал еще более актуальным. Конечно, в xml можно было использовать тэг и даже передать параметры с помощью DataBinding, а иногда удобнее и быстрее собрать новый компонент через copy/paste разметки xml похожих экранов (или элементов). Сейчас, когда UI переезжает в @Composable функции, в которых можно использовать всю силу Котлина, переиспользовать код проще, чем когда-либо. Только, пожалуйста, не забывайте о горячих клавишах Android Studio для этого (command+option+m или ctrl+alt+m для Mac/Windows).
Заключение
В нашем приложении мы используем архитектурный подход с MVI, в котором состояние едино, иммутабельно и данные двигаются в одном направлении (UDF — unidirectional data flow), поэтому бизнес-логика в ViewModel осталась практически нетронутой, и в этом плане внедрение Compose было достаточно простым. Одно из немногих опасений, которые еще остаются, заключается в том, что некоторые API фреймворка все еще экспериментальные (sticky headers для списков, отсутствие готового, «из коробки» ViewPager, отсутствие готовых анимаций для изменений в списках и т.д.). При использовании Jetpack Compose вам скорее всего придется столкнуться с тем, что некоторых привычных вещей в нем пока нет, так что придется писать свою реализацию или использовать API для включения существующих View (WebView и MapView — два самых популярных примера). При этом, учитывая все преимущества, которые мы получаем, очевидно, что стоит начать знакомство с Compose как можно скорее.
Авторы:
Роман Камышников, ведущий разработчик Android Центра компетенций мобильной разработки МТС Банка
Арсений Сафин, руководитель центра компетенций мобильной разработки МТС Банка
Сергей Кривопиша, технический лидер Android Центра компетенций мобильной разработки МТС Банка
Комментарии (8)
sergeymolchanovsky
10.11.2021 15:50-4Выглядит как очередное мертворожденное творение Google. Зачем этот псевдо-флаттер, когда и так есть Флаттер, который удобнее, с большим коммьюнити, и кросс-платформенный?
Reformat
10.11.2021 18:54+4Во первых Kotlin (строгая статическая компиляция) синтаксически мощнее чем Dart (разновидность JS) и коммьюнити у него побольше. Потому что Kotlin это не только Jetpack Compose, но и Java-библиотеки и весь классический Android SDK. Во вторых, на выходе получается родное для Android приложение, которое будет работать гораздо быстрее. И если уж нужно срочно закрыть необходимость в кроссплатформенном приложении, то имеет смысл сразу брать web-платформу с PWA, это гораздо проще чем Flutter.
Rikudoxxx
11.11.2021 02:35-1Сразу видно что вы ничего не знаете про Flutter и Dart и пишете отсебятину...
sergeymolchanovsky
15.11.2021 10:05+1@RikudoxxxХотел бы плюсануть, жаль, кармы не хватает.
Dart - разновидность JS... рукалицо. Дядя из 2011 вылез.
a15199732
12.11.2021 19:38Спасибо, на вес золота такие кейсы. Очень мало про Compose пока пишут.
Естественный отбор - лучшие практики перекочевывают из флаттера в JetPack, а "нелучшие" застрянут там :)
kirich1409
14.11.2021 15:17Вы совсем ничего не писали про опыт от работу с тулингом для Compose, а также как интегрировали тему между Compose и Android View.
Mox
Забавно смотрится iOSный переключатель на Android.
А как с производительностью? Там же тот же Skia внизу, что и во Flutter, то есть это отрисовка картинок, а не контролы операционки.
koperagen
Там та же Skia внизу, которая последние 10 лет рендерит контролы операционки :)
Впрочем, это лишь моя интерпретация слов из презентации Compose, которую я найти не смог, но есть вот такой пост на SO от инженера гугла.