image

Много было сказано про «красоту» кода на Java, но на мой взгляд, главное — не инструмент, а умение им пользоваться. Под катом попытка написать декларативный DSL для вёрстки под Android даже не изобретая новый язык программирования!

Вёрстка на Java всегда ассоциировалась у меня с болью.

float dp = getResources().getDisplayMetrics().density;

FrameLayout root = new FrameLayout(this);
root.setBackgroundColor(RED);

root.setLayoutParams(
        new ViewGroup.LayoutParams(
                MATCH_PARENT,
                (int)(100f*dp)));

FrameLayout child = new FrameLayout(this);
child.setBackgroundColor(GREEN);

FrameLayout.LayoutParams childParams =
        new FrameLayout.LayoutParams(
                (int)(50f*dp),
                (int)(50f*dp));

childParams.gravity = CENTER;

child.setLayoutParams(childParams);

root.addView(child);

Результат:



И дело даже не в том, что код выглядит страшно (а он страшный как чёрт). Основная проблема в том, что в нём невозможно не ошибиться. Я 3 раза перезаливал сюда этот код, в первый и второй разы наивно полагая, что смогу всё правильно написать сразу, и тщательно перепроверив всё лишь в третий. Скажете, что дело в моей невнимательности и будете правы, но если даже в такой простой вёрстке можно накосячить, то что уж говорить про что-то более сложное?

Но почему с вёрсткой на Java всё так грустно? На мой взгляд основная причина — возможность верстать в xml и отсутствие инструмента для вёрстки на Java.

Минусы xml


Для меня их 3.

Первый — оверхед.
Зачем тратить ресурсы и без того не очень мощных устройств на Android на такие операции, как inflate и findViewById? На оптимизацию этих операций было потрачено много времени и сил, но они от этого не стали бесплатными.

Второй — громоздкость.

<FrameLayout
    android:background="#f00"
    android:layout_width="match_parent"
    android:layout_height="100dp">

    <FrameLayout
        android:background="#0f0"
        android:layout_gravity="center"
        android:layout_width="50dp"
        android:layout_height="50dp"/>

</FrameLayout>


Удручает необходимость дублировать тэги, писать перед каждым атрибутом «android:», а после вырабатывать частиную слепоту чтобы читать этот код.

Третий — ограниченность языка.
Допустим, я хочу сделать подпись к автару больше самого аватара на 10dp.

<ImageView
    android:layout_width="@dimen/avatarSide"
    android:layout_height="@dimen/avatarSide"/>

<TextView
    android:layout_width="@dimen/avatarSide + 10dp"
    android:layout_height="wrap_content"/>


Но этого сделать нельзя потому что xml не поддерживает выражения.

Почему не Anko?


Anko — это DSL, с помощью которого можно декларативно описывать разметку на Kotlin.

frameLayout {

    backgroundColor = RED

    frameLayout {
        backgroundColor = GREEN
    }.lparams(dip(50), dip(50)) {
        gravity = CENTER
    }

}.lparams(matchParent, dip(100))

Получаем все возможности полноценного языка программирования, лучшую производительность и даже не мучаемся с вёрсткой интерфейса на Java!

Всё прекрасно, но, на мой взгляд, неприлично тянуть за собой целый рантайм языка при разработке библиотек. 500 кб — не так много для конечного приложения, но для библиотеки — явно перебор.

JAnko


Как оказалось, возможностей Java хватает чтобы верстать декларативно.

new frameLayout(this) {{

    new lparams(this) {{
        width  = MATCH_PARENT;
        height = dip(100);
    }}._();

    backgroundColor = RED;

    new frameLayout(this) {{

        new lparams(this) {{
            width  = dip(50);
            height = dip(50);
            gravity = CENTER;
        }}._();

        backgroundColor = GREEN;

    }}._();

}}._();

Язык поддерживает блоки кода без названия. Они выполняются перед конструктором класса сразу после конструктора класса-родителя.

class A {
    // block
    {
        // some code
    }
}

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

Пример с аватаркой и подписью.

new imageView(this) {{

    new lparams(this) {{
        width = dimen(R.dimen.avatarSide);
        height = dimen(R.dimen.avatarSide);
    }}._();

}}._();

new textView(this) {{

    new lparams(this) {{
        width = dimen(R.dimen.avatarSide) + dip(10);
        height = WRAP_CONTENT;
    }}._();

}}._();

Выглядит немного странно.



Похоже на человека в монокле и оператор на scala. Но для proof of concept — вполне достаточно.

Итоги


0). На Kotlin код выглядит вот так:

object : frameLayout(this) {
    init {

        object : lparams(this) {
            init {
                width = MATCH_PARENT
                height = dip(100f)
            }
        }.`_`()

        backgroundColor = RED

        object : frameLayout(this) {
            init {

                object : lparams(this) {
                    init {
                        width = dip(50f)
                        height = dip(50f)
                        gravity = CENTER
                    }
                }.`_`()

                backgroundColor = GREEN

            }
        }.`_`()

    }

}.`_`()

1) Вес aar составляет 12кб
2) Idea не сбивает форматирование
3) Коду на Java иногда можно придать неожиданный для Java вид

Репозиторий с библиотекой и примерами
Бенчмарк

Обычно чуть быстрее Anko, что забавно.

Я ожидал, что моя мини-библиотека станет последним пристанищем этого монстра, но даже оттуда её выпилил в пользу Litho, который выполняет measurment и layout в другом потоке. Спасибо eirnym за ссылку.
Закопать обратно?

Проголосовало 377 человек. Воздержалось 166 человек.

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

Поделиться с друзьями
-->

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


  1. Sirikid
    14.07.2017 06:07
    +3

    Первый — оверхед.

    А он точно есть?


    Кмк, верстать лучше на языке для верстки.


    1. adev_one
      14.07.2017 06:19

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


    1. vitalybaev
      14.07.2017 11:29
      +1

      В каком-то видео (вроде про Anko) говорили, что есть как минимум оверхед на парсинг xml. Попробую изучить вопрос


    1. Simipa
      17.07.2017 07:46

      Еще какой оверхед, особенно при использовании MVP:
      1) Мы пишем всю верстку на XML, прописывая тонны параметров, и еще и IDшники.
      2) Потом мы в активити еще цепляем все это (и лучше через butter knife, а то уж больно много писать)
      3) Потом мы пишем интерфейсы, которые будут управлять состоянием
      4) Оверрайдим интерфейс в активити, где в методах уже пишем логику, которая будет изменять состояние объектов (буквы, цифры, возможности нажатия кнопок)
      5) И, наконец, уже в презентаре мы будет вызывать методы интерфейса.

      В 5 разных местах мы по чуть чуть пишем логику 1(!) экрана. Я с радостью выброшу хотя бы 1 пункт из списка, и вообще откажусь от XML.


  1. elmal
    14.07.2017 07:38
    +1

    Никто не говорит, что на Java нельзя написать DSLи. Можно и нужно, и эти DSL могут быть вполне годные с точки зрения декларативности и компактности, если не нужны будут арифметические операции. Но все равно выглядит это как костыль. Двойные скобочки, ._(); оператор new и не следование конвенции что имя класса должно быть с большой буквы, передача this в параметры — это мусор, от которого не удалось избавиться.


    Конечно намного лучше, чем без DSL вообще. Но на anko это смотрится органично и изящно, там сам язык оптимизирован под такие DSL.


    1. Throwable
      14.07.2017 09:03
      +3

      Вобщем-то на Java ввиду особенностей языка как раз нельзя написать нормальный DSL. Лямбды немного улучшают ситуацию, но не сильно. В частности, проблемы возникают с описанием иерархической структуры и со ссылками на проперти (которые всегда будут не type-safe). Стандартрый способ — клепать билдеры, но с ними получается очень гормоздко. Поэтому практически всегда для описания структур используют сторонний язык типа XML.


      P.S. В Java 8 идентификатор _ объявлен как deprecated, а Java 9 вообще запрещает его использовать.


  1. hssergey
    14.07.2017 08:25
    +1

    Ну вообще стандартный подход — код на Java, а верстку — на языке соответствующего шаблонизатора — XML лэйауты для андроида, Thymeleaf или Velocity для спринга и прочих вэб-приложений и так далее. Это удобно тем, что можно разделить код и представление, которые могут делать разные разработчики. Формировать лэйауты или вэб-страницы на Java возможно, но намного неудобнее.

    Зачем тратить ресурсы и без того не очень мощных устройств на Android на такие операции, как inflate и findViewById

    Можно так делать, а можно использовать databinding: https://stfalcon.com/ru/blog/post/faster-android-apps-with-databinding. Ну а то, что сам шаблон на XML, я не вижу в этом ничего такого фатального: большая часть рутинных операций верстки делается в визуальном редакторе той же студии, а непосредственно в коде лэйаута правится только уже какие-то небольшие вещи.


  1. int00h
    14.07.2017 08:51

    Язык поддерживает блоки кода без названия. Они выполняются по очереди сразу после конструктора.

    Блок инициализации выполняется ДО каждого конструктора (а по факту копируется в него)


    Как оказалось, возможностей Java хватает чтобы верстать декларативно.
    Class {{
    //blah blah
    }}


    Только вот DSL в Anko != созданию инстанса анонимного класса каждый раз при объявлении элемента в верстке. В итоге вместо того, чтобы тратить ресурсы и без того не очень мощных устройств на Android на такие операции, как inflate и findViewById, тратим ресурсы на загрузку 1000 и 1 анонимного класса в память.


    1. adev_one
      14.07.2017 08:52
      -1

      Спасибо, поправил. После конструктора родителя и перед конструктором самого класса.


    1. adev_one
      14.07.2017 09:12
      +2

      А по поводу производительности, хороший вопрос, что затратнее: загрузка анонимного класса или обращение к ресурсу, которое является I/O операцией. Я пробенчмаркаю, когда появится минутка.


      1. Valle
        15.07.2017 07:43

        А загрузка анонимных классов это не I/O операции?


        1. imanushin
          15.07.2017 18:16

          Нет. Вся прогрузка подобных классов, связанная с IO, происходит при старте. Так что максимум возможных затрат — это jit компиляция.
          Т.е. в случае XML у нас будет IO + парсинг + выполнение, в данном случае — у нас просто выполнение (ибо байткод изначально сделан для удобства работы JVM, а не для чтения человеком, как xml)


          1. Valle
            15.07.2017 19:42

            Я считаю что подгрузка кода и подгрузка разметки этим кодом происходит при первом обращении к этому классу.


    1. adev_one
      18.07.2017 22:12

      Добавил бенчмарки. Дублирую ссылку. https://github.com/a-dminator/anko_benchmark


  1. abbath0767
    14.07.2017 10:00
    +3

    Громкий заголовок с бестолковым содержанием. Нет серьезно, неужели на хайпе котлина будут писаться и одобряться подобные статьи?


  1. impwx
    14.07.2017 10:33
    +2

    На тему декларативного фреймворка для описания Android-интерфейсов, посмотрите на Litho.


    1. adev_one
      14.07.2017 10:39

      Спасибо, действительно лаконично выглядит


  1. eirnym
    14.07.2017 10:40

    С инициализатором по-умолчанию есть очень большая проблема, связанная с контекстом. Дело в том, что полученный объект будет тянуть весь контекст класса, в котором он был вызван. Другими словами, в примере ниже если передать myvar куда-либо еще за пределы класса MyActivity, то он потащит всё активити с собой, что повлечёт неизбежную утечку памяти. При ревью я сразу делаю reject, видя такой код, поскольку дальнейшие часы на отлов утечек памяти, будет намного дороже.


    class MyActivity extends Activity {
      private Class myvar = new Class {{ /* code */ }}


    1. adev_one
      14.07.2017 10:43
      +1

      View, в любом случае, держит в себе контекст и ссылка на него даже не weak. Поэтому если передать View за пределы Activity, то это Activity неизбежно утечёт.


      1. eirnym
        14.07.2017 10:57

        Согласен, хотя я больше про то, что люди любят так часто писать, инициализируя и другие классы.


  1. vics001
    14.07.2017 10:58
    +1

    Возьмите groovy, это уже стандарт как писать DSL на Java. Как пример, gradle на чистом groovy написан. Да у groovy побольше библиотека, но на нем можно и другие DSL сделать и он не заточен под одну задачу.


  1. slavap
    14.07.2017 12:05
    +1

    Что мне нравится в написании UI кодом — это всегда работающие code completion and navigation. Лямбды сильно улучшают компактность, но xml всё равно легче читается, хотя может дело в привычке.


  1. Plesser
    14.07.2017 12:16

    Два момента. Первый момент: Google с Android Studio, как я понимаю, в сторону визуального создания интерфейсов (ConstraintLayout привет!). И идет оно туда (мы переходим ко второму моменту) для того что бы каждый занимался своим делом, дизайнеры рисовали интерфейсы а программисты реализовывали логику работы с ним. Это имхо.


  1. GerrAlt
    14.07.2017 13:53

    писать перед каждым атрибутом «android:»


    попробуйте посмотреть спецификацию xml Namespaces


    1. firegurafiku
      14.07.2017 18:17

      Поддержу.
      Вместо android: можно выбрать префикс покороче (например, a:), либо вообще установить пространством имён по умолчанию (тогда префиксы не понадобятся):


      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ...>
      <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" ...>
      <LinearLayout xmlns="http://schemas.android.com/apk/res/android" ...>

      С точки зрения XML — разницы никакой. Проблему с выражениями это, конечно же, не решает.


      1. asmrnv777
        15.07.2017 01:57

        А еще в IDEA/Android Studio прекрасный автокомплит, и вместо android:layout_height можно просто написать lh и нажать Enter. Так что проблема очень-очень притянута за уши.


  1. Foror
    14.07.2017 17:14
    +1

    Хочется сахорка? Держи:

    ui.frameLayout()
      .width(mathParent)
      .height(dip(100))
      .backgroundColor(RED)
      
      .child(x-> 
          x.frameLayout()
           .width(dip(50))
           .height(dip(50))
           .backgroundColor(GREEN)
           .gravity(CENTER)
       );
    

    И пожалуйста, не делай кастомизацию класса при создании объекта для сахарка (это я про двойные фигурные скобки), особенно на андроиде.