Много было сказано про «красоту» кода на 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 за ссылку.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (27)
elmal
14.07.2017 07:38+1Никто не говорит, что на Java нельзя написать DSLи. Можно и нужно, и эти DSL могут быть вполне годные с точки зрения декларативности и компактности, если не нужны будут арифметические операции. Но все равно выглядит это как костыль. Двойные скобочки, ._(); оператор new и не следование конвенции что имя класса должно быть с большой буквы, передача this в параметры — это мусор, от которого не удалось избавиться.
Конечно намного лучше, чем без DSL вообще. Но на anko это смотрится органично и изящно, там сам язык оптимизирован под такие DSL.
Throwable
14.07.2017 09:03+3Вобщем-то на Java ввиду особенностей языка как раз нельзя написать нормальный DSL. Лямбды немного улучшают ситуацию, но не сильно. В частности, проблемы возникают с описанием иерархической структуры и со ссылками на проперти (которые всегда будут не type-safe). Стандартрый способ — клепать билдеры, но с ними получается очень гормоздко. Поэтому практически всегда для описания структур используют сторонний язык типа XML.
P.S. В Java 8 идентификатор _ объявлен как deprecated, а Java 9 вообще запрещает его использовать.
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, я не вижу в этом ничего такого фатального: большая часть рутинных операций верстки делается в визуальном редакторе той же студии, а непосредственно в коде лэйаута правится только уже какие-то небольшие вещи.
int00h
14.07.2017 08:51Язык поддерживает блоки кода без названия. Они выполняются по очереди сразу после конструктора.
Блок инициализации выполняется ДО каждого конструктора (а по факту копируется в него)
Как оказалось, возможностей Java хватает чтобы верстать декларативно.
Class {{ //blah blah }}
Только вот DSL в Anko != созданию инстанса анонимного класса каждый раз при объявлении элемента в верстке. В итоге вместо того, чтобы тратить ресурсы и без того не очень мощных устройств на Android на такие операции, как inflate и findViewById, тратим ресурсы на загрузку 1000 и 1 анонимного класса в память.adev_one
14.07.2017 08:52-1Спасибо, поправил. После конструктора родителя и перед конструктором самого класса.
adev_one
14.07.2017 09:12+2А по поводу производительности, хороший вопрос, что затратнее: загрузка анонимного класса или обращение к ресурсу, которое является I/O операцией. Я пробенчмаркаю, когда появится минутка.
Valle
15.07.2017 07:43А загрузка анонимных классов это не I/O операции?
imanushin
15.07.2017 18:16Нет. Вся прогрузка подобных классов, связанная с IO, происходит при старте. Так что максимум возможных затрат — это jit компиляция.
Т.е. в случае XML у нас будет IO + парсинг + выполнение, в данном случае — у нас просто выполнение (ибо байткод изначально сделан для удобства работы JVM, а не для чтения человеком, как xml)Valle
15.07.2017 19:42Я считаю что подгрузка кода и подгрузка разметки этим кодом происходит при первом обращении к этому классу.
adev_one
18.07.2017 22:12Добавил бенчмарки. Дублирую ссылку. https://github.com/a-dminator/anko_benchmark
abbath0767
14.07.2017 10:00+3Громкий заголовок с бестолковым содержанием. Нет серьезно, неужели на хайпе котлина будут писаться и одобряться подобные статьи?
eirnym
14.07.2017 10:40С инициализатором по-умолчанию есть очень большая проблема, связанная с контекстом. Дело в том, что полученный объект будет тянуть весь контекст класса, в котором он был вызван. Другими словами, в примере ниже если передать
myvar
куда-либо еще за пределы классаMyActivity
, то он потащит всё активити с собой, что повлечёт неизбежную утечку памяти. При ревью я сразу делаю reject, видя такой код, поскольку дальнейшие часы на отлов утечек памяти, будет намного дороже.
class MyActivity extends Activity { private Class myvar = new Class {{ /* code */ }}
adev_one
14.07.2017 10:43+1View, в любом случае, держит в себе контекст и ссылка на него даже не weak. Поэтому если передать View за пределы Activity, то это Activity неизбежно утечёт.
eirnym
14.07.2017 10:57Согласен, хотя я больше про то, что люди любят так часто писать, инициализируя и другие классы.
vics001
14.07.2017 10:58+1Возьмите groovy, это уже стандарт как писать DSL на Java. Как пример, gradle на чистом groovy написан. Да у groovy побольше библиотека, но на нем можно и другие DSL сделать и он не заточен под одну задачу.
slavap
14.07.2017 12:05+1Что мне нравится в написании UI кодом — это всегда работающие code completion and navigation. Лямбды сильно улучшают компактность, но xml всё равно легче читается, хотя может дело в привычке.
Plesser
14.07.2017 12:16Два момента. Первый момент: Google с Android Studio, как я понимаю, в сторону визуального создания интерфейсов (ConstraintLayout привет!). И идет оно туда (мы переходим ко второму моменту) для того что бы каждый занимался своим делом, дизайнеры рисовали интерфейсы а программисты реализовывали логику работы с ним. Это имхо.
GerrAlt
14.07.2017 13:53писать перед каждым атрибутом «android:»
попробуйте посмотреть спецификацию xml Namespacesfiregurafiku
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 — разницы никакой. Проблему с выражениями это, конечно же, не решает.
asmrnv777
15.07.2017 01:57А еще в IDEA/Android Studio прекрасный автокомплит, и вместо android:layout_height можно просто написать lh и нажать Enter. Так что проблема очень-очень притянута за уши.
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) );
И пожалуйста, не делай кастомизацию класса при создании объекта для сахарка (это я про двойные фигурные скобки), особенно на андроиде.
Sirikid
А он точно есть?
Кмк, верстать лучше на языке для верстки.
adev_one
Нужно или дублировать объявление каждого виджета, или каждый раз проходить по всей иерархии вьюх в поисках нужного. Плюс неудобно когда объявление части вью находится в одном месте, а другая часть — в другом. Имхо, хорошо верстать на языке для вёрстки, если на нём действительно можно описать всю вёрстку.
vitalybaev
В каком-то видео (вроде про Anko) говорили, что есть как минимум оверхед на парсинг xml. Попробую изучить вопрос
Simipa
Еще какой оверхед, особенно при использовании MVP:
1) Мы пишем всю верстку на XML, прописывая тонны параметров, и еще и IDшники.
2) Потом мы в активити еще цепляем все это (и лучше через butter knife, а то уж больно много писать)
3) Потом мы пишем интерфейсы, которые будут управлять состоянием
4) Оверрайдим интерфейс в активити, где в методах уже пишем логику, которая будет изменять состояние объектов (буквы, цифры, возможности нажатия кнопок)
5) И, наконец, уже в презентаре мы будет вызывать методы интерфейса.
В 5 разных местах мы по чуть чуть пишем логику 1(!) экрана. Я с радостью выброшу хотя бы 1 пункт из списка, и вообще откажусь от XML.