Compose. Jetpack Compose

    image

    Благодаря стремительному развитию мобильной индустрии каждые несколько лет мы наблюдаем появления новых технических решений, призванных усложнить упростить жизнь разработчикам. Некоторые из них, не сыскав популярности у пользователей, остаются лишь частью истории, другие – плотно укореняются в повседневной разработке, становясь в определенной области стандартом де-факто.

    Пожалуй, главным трендом мобильной разработки за последние несколько лет стал декларативный UI. Такое решение уже давно успешно применяется в веб и кроссплатформенных решениях и, наконец, добралось и до нативной разработки. На iOS существует SwiftUI (представленный на WWDC 2019), а на Android – Jetpack Compose (представленный месяцем ранее на Google I/O 2019). И именно о последнем мы сегодня и поговорим.

    Примечание: в данной статье мы не будем рассматривать поэтапное создание первого проекта на Compose, так как этот процесс прекрасно описан в других материалах. Моя цель – лишь рассказать о преимуществах и недостатках, которые дает android-разработчикам переход на Jetpack Compose, а решение использовать или нет всегда остаётся за вами.

    Появление


    Официальная история Jetpack Compose начинается с мая 2019, когда он был представлен публике на конференции Google I/O. «Простой, реактивный и Kotlin-only» – новый декларативный фреймворк от Google выглядел как младший брат Flutter (который к тому моменту уже стремительно набирал популярность).

    API design is building future regret

    О недостатках текущего UI-фреймворка Android было сказано и написано уже достаточно большое количество раз. Проблемы с View-иерархией, зависимость от релизов платформы – наличие этих и множества других мелких недостатков в той или иной мере доставляли неудобства разработчикам, что и побудило компанию Google заняться разработкой нового фреймворка, способного решить все эти проблемы.

    Преимущества


    Итак, чем же хорош Jetpack Compose и, главное, чем он кардинально отличается от существующего на данный момент UI-фреймворка Android?

    • Unbundled toolkit: JC не зависит от конкретных релизов платформы, а значит, забудем уже про Support Library.
    • Kotlin-only: Больше не нужно переключаться между классами и xml-файлами – вся работа с UI происходит в одном Kotlin-файле.
    • Композитный подход: Наследованию – нет, композиции – да. Каждый UI-компонент представляет собой обычную composable-функцию, отвечающую только за ограниченный функционал, т.е. без лишней логики. Никаких больше View.java на 30 тысяч строк кода.
    • Unidirectional Data Flow: Одна из основополагающих концепций Jetpack Compose, о которой будет рассказано подробнее чуть ниже.
    • Обратная совместимость: Для использования Compose не требуется начинать проект с нуля. Имеется возможность как его встраивания (с помощью ComposeView) в имеющуюся xml-вёрстку, так и наоборот.
    • Меньше кода: Тут, как говорится, «лучше один раз увидеть, чем сто раз услышать». В качестве примера возьмём классическое сочетание компонентов – два поля ввода и кнопка подтверждения:

    В реализации текущего UI-фреймворка вёрстка этих компонентов выглядит так:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical"
        android:padding="@dimen/padding_16dp">
    
        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/til_login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
            android:hint="@string/sign_in_email"
            android:layout_marginBottom="@dimen/margin_8dp">
    
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_login"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="text"/>
    
        </com.google.android.material.textfield.TextInputLayout>
    
        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/til_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
            android:hint="@string/sign_in_password"
            android:layout_marginVertical="@dimen/margin_8dp">
    
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textPassword"/>
    
        </com.google.android.material.textfield.TextInputLayout>
    
        <Button
            android:id="@+id/btn_confirm"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/sign_in_submit"
            android:layout_marginTop="@dimen/margin_8dp"
            android:padding="@dimen/padding_8dp"
            android:background="@color/purple_700"/>
    
    </LinearLayout>
    

    В то же время, при использовании Jetpack Compose, решение будет выглядеть следующим образом:

    @Preview
    @Composable
    fun LoginPage(){
        var loginValue by remember { mutableStateOf(TextFieldValue("")) }
        var passwordValue by remember { mutableStateOf(TextFieldValue("")) }
    
        Surface(color = Color.White) {
            Column(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
                Surface(color = Color.White, modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp))) {
                    OutlinedTextField(
                            value = loginValue,
                            onValueChange = { loginValue = it },
                            label = { Text(text = stringResource(id = R.string.sign_in_email)) },
                            placeholder = { Text(text = stringResource(id = R.string.sign_in_email)) },
                            modifier = Modifier.fillMaxWidth()
                    )
                }
                Surface(color = Color.White, modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp))) {
                    OutlinedTextField(
                            value = passwordValue,
                            onValueChange = { passwordValue = it },
                            label = { Text(text = stringResource(id = R.string.sign_in_password)) },
                            placeholder = { Text(text = stringResource(id = R.string.sign_in_password)) },
                            visualTransformation = PasswordVisualTransformation(),
                            modifier = Modifier.fillMaxWidth()
                    )
                }
                Button(
                        onClick = {},
                        modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp)).fillMaxWidth(),
                        backgroundColor = colorResource(R.color.purple_700)) {
                    Text(text = stringResource(id = R.string.sign_in_submit), modifier = Modifier.padding(8.dp))
                }
            }
        }
    }
    

    Ну и напоследок – сравнительный результат:

    image

    Недостатки


    • Alpha-версия: Безусловно, более чем за год разработки фреймворк значительно преобразился и стал гораздо стабильнее. Однако это всё ещё альфа, а поэтому за пределами Pet-проектов использовать его не рекомендуется.

    Декларативный стиль


    Отдельное внимание стоит уделить главной особенности Jetpack Compose – декларативному стилю создания UI. Суть подхода заключается в описании интерфейса как совокупности composable-функций (они же виджеты), которые не используют «под капотом» view, а напрямую занимаются отрисовкой на canvas. Для кого-то это минус, для других – возможность попробовать что-то новое. Так или иначе, к концепции «верстать UI кодом» нативному разработчику, не работавшему ранее с аналогичными технологиями (к примеру, Flutter или React Native), придётся привыкать.

    Что за Unidirectional Data Flow?


    В современном android-приложении UI-состояние меняется в зависимости от приходящих событий (нажатие на кнопку, переворот экрана и т.д.). Мы нажимаем на компонент, тем самым формируя событие, а компонент меняет свой state и вызывает callback в ответ. Из-за довольно тесной связи UI-состояния с View это потенциально может привести к усложнению поддержки и тестирования такого кода. К примеру, возможна ситуация, когда помимо внутреннего state компонента, мы можем хранить его состояние в поле (например во viewmodel), что теоретически может привести к бесконечному циклу обновления этого самого state.

    Что же касается Jetpack Compose, то здесь все компоненты по умолчанию являются stateless. Благодаря принципу однонаправленности нам достаточно «скормить» модель данных, а любое изменение состояния фреймворк обработает за нас. Таким образом, логика компонента упрощается, а инкапсуляция состояния позволяет избежать ошибок, связанных с его частичным обновлением. В качестве примера возьмем уже рассмотренный ранее composable-код. Перед описание компонентов были определены две переменные:

        var loginValue by remember { mutableStateOf(TextFieldValue("")) }
        var passwordValue by remember { mutableStateOf(TextFieldValue("")) }
    

    Мы создаем два текстовых объекта, значения которых будем устанавливать полям ввода (логина и пароля) в качестве value. А благодаря связке remember { mutableStateOf(…) } любое изменение значений этих объектов (из других частей кода) уведомит об этом соответствующее поле ввода, которое перерисует только значение value, вместо полной рекомпозиции всего компонента.

    Вывод


    Какой же вывод можно сделать о Jetpack Compose? По моему мнению, у нового решения от Google имеется огромный потенциал. С момента анонса в 2019 году была проделана огромная работа, и не менее долгий путь до релиза у фреймворка ещё впереди. Однако теперь он публично доступен, и я считаю, что это прекрасная возможность познакомиться с ним поближе. Ну а за чем, по вашему мнению, будущее – пишите в комментарии, будет интересно узнать ваше мнение. Любите android!
    EPAM
    Компания для карьерного и профессионального роста

    Комментарии 12

      +1
      Отличная статья, все четко и по делу, добавлю в закладки
        0
        Спасибо за комментарий! Приятно, что вас заинтересовала статья.
        0
        Прям очень похоже на SwiftUI. В процессе изучения Android разработки очень не нравится xml верстка, а здесь все просто и наглядно.
          0
          Да, вы абсолютно правы, многие декларативные фреймворки так или иначе похожи друг на друга. И я думаю это хорошо, так как заимствование друг у друга уже проверенных и успешных приёмов способствует унификации декларативной разработки в целом.
          0
          Композитный подход: Наследованию – нет, композиции – да. Каждый UI-компонент представляет собой обычную composable-функцию, отвечающую только за ограниченный функционал, т.е. без лишней логики. Никаких больше View.java на 30 тысяч строк кода.


          Как то очень тяжело вериться в такие радужные заявления
            +1
            В данном конкретном примере xml вёрстка выглядит значительно чище и читабельней. Более того, namespace android можно импортировать, тогда не нужно будет каждый раз аттрибуты префиксовать.
              0
              Согласен, в контексте простого фрагмента авторизации преимущество Jetpack Compose возможно кажется не таким явным. Однако, к сожалению, сегодня даже не самые сложные экраны обладают куда большей вёрсткой, и в этом случае преимущество декларативного UI будет очевиднее.
                0
                Время покажет, пока композ выглядит как очередная гламурная хрень которую надо продать гламурным кодерам.
                  0

                  Смотрю на твитче блог Leland Richardson, которые в Гугле занимается компиляторным плагином для Compose и параллельно dogfooding. Ну так я бы сказал, что и для сложных сцен на данном этапе всё не просто. Т.е. одинаковые вещи на compose сейчас сделать даже сложнее.

                  0

                  Всё верно. Но не забывайте, что текущий вариант вылизывался 10+ лет под типичные юзкейсы приложений.

                    0
                    Я во общем-то не против, наверняка, в каких-то случаях будет преимущество. Но меня реально пугает в последнее время тенденция переизобретать велосипеды, лишь бы на новом языке. Классический пример — многие не смогли осилить SOAP/XML, взяли модный молодёжный rest/json, потом через какое-то время поняли, что без схем тяжело в общем-то жить и переизобрели SOAP в виде json-schema+swagger/openapi (впрочем, я подозреваю, что из браузеров проще работать с json)
                    0
                    Более того, namespace android можно импортировать, тогда не нужно будет каждый раз аттрибуты префиксовать.
                    Это как это?

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое