Про Jetpack Compose на сегодняшний день слышал, пожалуй, каждый android-разработчик. Некоторым уже удалось «затащить его в прод», кто-то пробовал его в своих пет-проектах, а кто-то до сих пор сомневается в его целесообразности. Ведь на первый взгляд мы имеем все тоже самое: <TextView> заменили на Text(), Box() очень похож на <FrameLayout>  и др. Единственное, что сразу произвело впечатление — новые Lazy-списки, которые являются заменой привычного RecyclerView и позволяют писать меньше кода. Но, опять же, скопировать и подправить адаптер под очередной экран это уже привычное действие и оно не занимает много времени, а само действие отработано до автоматизма. К числу сомневающихся и, возможно совсем немного lazy, можно было отнести и меня. Но несколько задач, отличных от рутинного перекрашивания кнопок, заставили меня пересмотреть свое отношение к Compose. 

Случай первый. Дано: макет в фигме следующего вида

Если у вас, как и у меня в свое время, появились сомнения в увиденном, то скорее вы все поняли правильно. Необходимо реализовать таблицу с горизонтальным скроллом, при этом первая ячейка должна быть жестко фиксирована. Зачем? «Увидели, понравилось, хотим себе». 

Как такое делать я не знал. Stackoverflow выдал несколько решений, от которых я начал нервничать. В одном из них была реализация через <Table>. Когда про него вспоминали последний раз? Другие решения были на базе RecyclerView (уже лучше) — кастомные LayoutManager или декораторы. Но главная причина, по которой я отверг эти решения, — ячейки имели статические размеры, а в моем случае высота зависит от текста в первой колонке. Желания прокидывать размеры и пересчитывать высоты ячеек в списке у меня не было, и я снова вернулся к гуглу.

На этом этапе я решил не игнорировать ссылки, которые вели на статьи с Jetpack Compose. Оказывается в LazyColumn/LazyRow помимо item-элементов были добавлены (пока еще Experimental) stickyHeaders (https://developer.android.com/jetpack/compose/lists#sticky-headers). 

Собственно это та штука, которая «прилипает» к верху экрана при скролле. Данная находка открыла во мне второе дыхание. Я посчитал, что для реализации задуманного будет удобно ответ сервера представить ввиде нескольких списков, по списку на каждый столбец.

Тогда начало нашей Compose-функции (назовем ее по-простому DataTable) будет выглядеть так:

Теперь опишем элементы таблицы, их всего три: текстовый хедер, картинка с отображением статуса и ячейка с галочкой/ без галочки.

Время строить таблицу:

(я Lazy, а еще и Sticky!)
(я Lazy, а еще и Sticky!)

Используется LazyRow, так как нужен именно горизонтальный скролл. В stickyHeader задаем фиксированный столбец. В itemsIndexed будем отрисовывать остальные столбцы с «галочками». Если запустить наш код на данном этапе, то экран будет выглядеть вот так:

Некрасиво.

Как видно, изначальная проблема с высотами ячеек при таком подходе все равно остается, поэтому поищем способ ее решения. У Modifier в доступных функциях есть колбэк onGloballyPositioned. Он возвращает координаты элемента на экране после его отрисовки и через них мы будем находить высоту. Для «прокидывания» высот в другие ячейки я использовал mutableState:

Ключ - индекс элемента(можно позволить, поскольку кол-во ячеек во всех столбцах одинаково), значение - высота.

Конечный код LazyRow элемента будет иметь вид: 

Первым создается stickyHeader. После заполнения ячеек контентом, высчитываются высоты и сохраняются в mutableState. 

После отрисовываются столбцы с «галочками». Из mutableState берутся высоты. В верху столбца (для index == 0) ставится картинка.

Запускаем, получаем такой экран, радуемся результату:

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

Посмотреть как работает можно тут: https://play.google.com/store/apps/details?id=su.art.spbrealty&hl=ru 

Случай второй. Буквально через пару недель мне поручили «небольшой» редизайн экрана.  Из макета видно, что планируется блок со списком услуг по категроиям. При скролле он фиксируется вверху, в нем перелистываются категории, пор нажатию на категорию мы скроллим до нужной позиции в списке услуг:

В stickyHeader можно поместить любую @Composable - функцию, а значит подход из первого случая будет работать и здесь. Список будет иметь вид:

Сначала размещаем элементы, которые будут скрываться под тулбаром, затем stickyHeader, после - все остальное.

По итогу имеем такой экран:

Посмотреть можно здесь : https://play.google.com/store/search?q=мой%20теле2&c=apps&hl=ru

Случай третий. Кнопка. В данном случае было необходимо разработать экран объявления. При создании макета дизайнеры вдохновлялись Авито, в частности поведением кнопки «написать автору объявления»:

Длинный экран со скроллом. У кнопки «Написать» есть свое место на этом экране. Но если пользователь не дошел до этого места,  то кнопка должна быть видна поверх контента. 

Первой мыслью было поискать в документации stickyFooter, поскольку это как stickyHeader, но внизу. Но, к сожалению, разработчики Google не посчитали нужным такой элемент. И тут у меня появилась безумная идея. 

Что будет, если у LazyColumn задать параметр reverseLayout = true? Будет ли stickyHeader «липнуть» к низу экрана? Как оказалось — будет. Дело за малым: переворачиваем порядок в списке задом наперед и получаем желаемый вид экрана:

Если вы еще не используете Compose в своих проектах, то определенно стоит подумать о том, что бы начать, поскольку он позволяет делать сложные вещи простыми и, что самое главное, экономит время (и нервы).

Хочу надеяться, что мой опыт окажется полезным и поможет другим разработчикам в создании нестандартных экранов. И не бойтесь изучать что-то новое. Возможно когда-нибудь это новое сделает вашу жизнь чуточку легче.

Статью подготовил Илья Кубышкин, Android- разработчик в e-legion

Комментарии (5)


  1. D7ILeucoH
    24.04.2024 18:29

    Хех, статья интересная, увидел для себя интересный способ работы с ремембером для сохранения размера одних вьюх, чтобы потом проецировать это на другие вьюхи.

    Но что насчёт ремемберинга картинок? Сколько бы не пытался следовать гайдлайнам, картинка каждый раз читается. То есть не кешируется, как в ресайклере. Из-за чего на экранах со множеством больших картинок возникают лаги при скролле.

    С этой проблемой как-то боретесь? Я видел решение в виде специальной либы Coil, но оно не KMM, поэтому нежелательно...

    А насчёт костыля со sticky header как футером - нее, всё таки не стоит так делать, это очень сложно потом поддерживать. Тут либо сделать своё решение (исходники компоуза открыты), либо взять чужую либу (думаю должна быть).


    1. kubik92
      24.04.2024 18:29

      Пробовали для списков использовать иммутабельные коллекции от джетбрейнс? По дефолту List это нестабильный тип и из-за него может происходить рекомпозиция лишний раз. Либо заменить на PersistentList, либо попробовать вьюстейт пометить аннотацией Stable или Immutable (если в целом там находятся нестабильные типы данных).
      По поводу костыля - полностью согласен, но порой, у каждого возникают ситуации, когда приходится делать то, чего не очень хочется, особенно когда поджимают сроки :). Тут скорее просто забавный факт, что оно работает вот таким образом, если список перевернут.


    1. OlimzhanovUmid
      24.04.2024 18:29

      Coil вроде бы в третьей версии уже поддерживает мультиплатформу, правда он ещё в альфе


  1. OlimzhanovUmid
    24.04.2024 18:29

    Чтобы сделать кнопку как в третьем случае, можно использовать floatingActionButton у Scaffold'а.


    1. OlimzhanovUmid
      24.04.2024 18:29

      Upd: Или же просто box с фиксированной внутри кнопкой. Со вложенными Scaffold иногда проблемы.