Пару месяцев назад
перепала мне задачка пилить в одиночку
довольно объемный проект на Android. Сам
по себе проект был несложный, но количество
повторяющегося кода зашкаливало. При
том самое обидное, что стандартными
средствами как-то избавиться от повторений
не представляется возможным. Тогда я и
наваял первоначальную версию небольшой
генерилки андроид-кода.
Больше всего, конечно,
в андроиде меня раздражает xml. Логично,
что формочки лучше описывать в
декларативном стиле; понятно, что java
для этого не годится; разумеется, xml
здесь уместен. Но, блин, как же отстойно
все это выглядит! Элементарного цикла
в нем не сделать, а уж про реюзабельность
вообще молчу. Она там, конечно, есть в
виде подключаемых пользовательских
компонентов. Но все эти механизмы страшно
сосут даже по сравнению с инструментами
разработки под iPhone!
Самое главное, чего я
хотел добиться созданием своего DSL, –
это избавиться от богомерзкого xml. Я
хотел описывать UI в
декларативном стиле на языке общего
назначения. Оказалось, что на Clojure – это
очень даже просто, достаточно
воспользоваться библиотекой hiccup.
Например, вместо этого
<Button android:id="@+id/helloWorldBtn" android:background="@drawable/blue_button_slt" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="Push me!" android:textColor="#fff" android:textSize="10dp" android:textStyle="bold"/>
писать такой код:
(Button "helloWorldBtn" wc wc [:text "Push me!" :textColor "#fff" :textSize "10dp" :textStyle "bold" :background (drawable "blue_button_slt")])
На данном этапе выигрыша
в коде почти никакого нет. Зато уже можно
сделать, например, так:
(defn large-button [id attributes image & [ txt]] (VLayout nil wc wc (conj attributes :gravity "center") (ImageButton id "60dp" "70dp" [:background (drawable image)]) (if-not (empty? txt) (TextView nil "125dp" "18dp" [:text txt :textSize "16dp" :gravity "center" :textColor "#0096c1"]))))
Как видите, введение
языка общего назначения вместо xml
позволяет легко создавать функции.
Напротив, создание реюзабельного
компонента под андроид – процедура
более долгая и трудоемкая. Еще очень
удобно использовать if-ы, loop-ы и прочие
возможности языка. Кроме того, код на
Clojure писать проще, ведь можно заранее
задать для компонентов сокращенные
варианты атрибутов либо рациональные
настройки. Например, в приведенном выше
исходнике wc означает "wrap_content".
Кроме генерации xml на
основе приведенного выше примере, можно
сгенерировать и код на java. Это позволит
автоматизировать многие, часто повторяемые
операции.
Рассмотрим пример формы
с текстовой строкой и двумя кнопками.
(defn layout [] (html (VLayoutFP nil [:background (drawable "home_bgr")] (HLayoutFP nil [] (VLayoutFP nil [] (TextView nil wc wc [:text "Demo text" :textColor "#09bbaa"]) (Button "helloWorldBtn" wc wc [:text "Push me!" :textColor "#fff" :textSize "10dp" :textStyle "bold" :background (drawable "blue_button_slt") :code (->java {} (def ^:String hello "Hello") (def world "world") (System.out.println (+ hello " " world)))]) (Button "demoBtn" wc wc [:text "Push me!" :textColor "#fff" :textSize "10dp" :textStyle "bold" :background (drawable "blue_button_slt") :code (->java {}
(startActivity (new android.content.Intent this OrderScreen.class)))]) )))))
(generate-activity layout "com.adsl.demo" "MyDemoActivity")
Будет создана связка
файлов MVC. Генерятся классы
MyDemoActivityModel, MyDemoActivityView, MyDemoActivityController и
связываются таким образом, что они сразу
готовы к использованию как единая
сущность. Будет создана заготовка
метода, который вызывается во вьюхе
при изменении модели.
public void update(Observable modelObj, Object arg) { initialize(); MyDemoActivityModel model = (MyDemoActivityModel)modelObj; }
В классе MyDemoActivityView
будут созданы атрибуты:
private Button _helloWorldBtn; private Button _demoBtn;
Позже они инициализируются
таким образом:
_helloWorldBtn = (Button) findViewById(R.id.helloWorldBtn); _demoBtn = (Button) findViewById(R.id.demoBtn);
Кнопкам назначается листенер, который вызывает соответствующий метод контроллера:
View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { if (v.equals(_helloWorldBtn)) { controller.helloWorldBtnActionPerformed(MyDemoActivityView.this); return; } if (v.equals(_demoBtn)) { controller.demoBtnActionPerformed(MyDemoActivityView.this); return; } } }; _helloWorldBtn.setOnClickListener(listener); _demoBtn.setOnClickListener(listener);
Еще одна забавная штука,
которую я добавил скорее по приколу, –
это компиляция из моего псевдо-лиспа в
java. Кнопке можно задать атрибут :code и
прописать туда такое выражение:
(->java {} (def ^:String hello "Hello") ; Объявить стринговую переменную (def world "world") ; Объявить нетипизированную переменную (System.out.println (+ hello " " world))) ; Вызвать функцию с параметрами
Генератор преобразует
эти выражения в java-код и вставит его в
тело метода:
public void helloWorldBtnActionPerformed(MyDemoActivityView view) { String hello = "Hello"; Object world = "world"; System.out.println(hello + " " + world); }
Вообще, такой транслятор
с псевдо-лиспа в java мне понадобился в
основном затем, чтобы можно было по
нажатию кнопки сделать простую
инициализацию объекта либо показать
другой активити.
Например:
(->java {} (startActivity (new android.content.Intent this OrderScreen.class)))
На выходе будет:
public void demoBtnActionPerformed(MyDemoActivityView view) { startActivity(new android.content.Intent(this, OrderScreen.class)); }
Сам транслятор содержится в файле clj2java.clj, а главная функция выглядит так:
(defn process ([data raw-code] (let [code (enrich-with-data data raw-code)] (if (list? code) (let [elem1 (first code)] (cond (= elem1 'def) (process-definition code) (= elem1 'defn) (process-func-definition code) (= elem1 'do) (process-command-sequence (next code)) (= elem1 'set!) (process-assignment (next code)) (= elem1 'if) (process-if (next code)) (= elem1 'return) (process-return (next code)) (= elem1 '->) (process-method-call (next code)) (= elem1 'lambda-interface) (process-lambda-interface) (= elem1 'lambda) (process-lambda code) (= elem1 'call) (process-call (next code)) (= elem1 'loop) (process-loop (next code)) (= elem1 'new) (process-new (next code)) :else (process-function-call code))) (process-single-value code)))) ([raw-code] (process {} raw-code)))
Как видите, транслятор
умеет не так уж и мало и позволяет писать
более краткий, по сравнению с java, код.
Сам же язык описания
интерфейсов построен с применением
барьеров абстракции, как об этом
рассказывается в SICP и On Lisp. Весь код
содержится в файле util.clj.
На нижнем уровне
абстракции находятся различные служебные
функции и ядро всех будущих компонентов
– функция виджет:
(defn widget [type id width height params-raw & more] . . . )
Все прочие виджеты
создаются на основе этой функции,
например, так:
(defn Button [id width height & [params]] (widget "Button" id width height (apply android params))) (defn HLayoutWC [id & [params & more]] (widget "LinearLayout" id "wrap_content" "wrap_content" (merge {"xmlns:android" "http://schemas.android.com/apk/res/android" (andr-attr "orientation") "horizontal"} (apply android params)) more))
Верхний уровень
абстракции – кастомные виджеты и
хелперы, которые создаются на основе
стандартных виджетов. Например, синяя
кнопка выглядит так:
(defn BlueButton [id width height & [params]] (Button id width height (flatten (conj params [:background (drawable "blue_button_slt") :textColor "#fff" :textSize "10dp" :textStyle "bold"]))))
А хелперы для отступа
выглядят так:
(defn padding-top [top & more] (HLayout nil wc wc [:paddingTop top] more)) (defn padding-left [left & more] (HLayout nil wc wc [:paddingLeft left] more))
Саму генерилку описывать
особо нет смысла, т.к. она достаточно
простая.
Плюсы подхода:
- Сокращение кода описания активити в три-четыре раза. Чем больше и сложнее описываемая форма – тем больше выигрыш.
- Гораздо более простой способ создания абстракций при помощи обыкновенных функций. В результате значительно повышается реюзабельность кода.
- Значительно облегчается чтение и модификация кода UI по сравнению с использованием xml.
Минусы:
- Сгенеренный код выглядит, порой, написанным copy-paste-ом.
- При перегенерации кода теряется код, написанный вручную в сгенеренных ранее файлах.
Вообще, я с минусами
придумал бороться следующим образом.
Во-первых, не лазить в сгенеренные файлы
вообще. Если нужно добавить тело метода
или еще какую функциональность, то нужно
наследоваться от сгенеренных классов.
Это избавляет от необходимости переносить
вручную набитый код в новую версию
нагенеренных классов. Кроме того, раз
не нужно больше лазить в сгенеренные
классы, то отпадает требование о красоте
их кода, и автоматический copy-paste становится
допустимым. Главное, чтобы его не было
на уровне самого DSL.
Я получил эстетическое удовольствие! Спасибо за красивую работу! Успехов!
ОтветитьУдалитьСпасибо! :-)
ОтветитьУдалить