Redmadrobot corporate blog
Development of mobile applications
Development for Android
January 2016 12

Тотальная шаблонизация

Tutorial


Когда собаке программисту нечего делать, он начинает все автоматизировать. Мне по роду своей деятельности приходится писать много кода и, конечно, хочется какие-то повторяющие вещи обобщить в виде библиотек, скриптов или шаблонов для Android Studio. О них и поговорим.



Шаблоны — кто они?


Шаблон в терминах Android Studio это файл (или набор файлов) с расширением .ftl, содержащий конструкции на Java и XML (зависит от решаемой задачи), а также метаконструкции на языке шаблонизатора (template engine). Шаблонизатором в нашем случае выступает FreeMarker, язык которого является простым, но в то же время достаточно мощным для написания сложных шаблонов.

В нашем распоряжении уже есть куча разных шаблонов: activity, fragments, services, widgets, UI-компоненты, директории и многое другое. Но бывают случаи (такие как наш), когда существующих шаблонов недостаточно и нужно делать свой. И тут нас постигает первое “удивление”: информации по этому делу очень мало. Есть несколько статей в блогах и чудом извлеченная из недр GoogleGit скудная документация.

Осмотр территории


Учиться лучше всего на примерах, поэтому будем разбирать достаточно простой, но в то же время содержащий все важные аспекты шаблон пустого фрагмента. Добыть его можно из ANDROID_STUDIO_DIR/plugins/android/lib/templates/other/BlankFragment. Лучше куда-нибудь скопировать содержимое этого каталога, чтобы ничего не сломать своими экспериментами. Для начала разберемся с тем, что там есть.
  • globals.xml.ftl — набор глобальных для шаблона переменных
  • root/res/layout/fragment_blank.xml.ftl — шаблон разметки фрагмента
  • root/res/values/strings.xml — строковые константы, которые будут добавлены в проект
  • root/src/app_package/BlankFragment.java.ftl — шаблон кода
  • template.xml — метаданные шаблона
  • template_blank_fragment.png — самый важный файл! Пиктограмма фрагмента в данном случае


Понять, что происходит в каждом из файлов, не составляет труда. Проблемы начинаются, когда возникает необходимость кастомизировать происходящее в них или даже вообще написать что-то свое с нуля. В этом немного может помочь официальная документация

А где же реальные примеры?!


Разобраться в нюансах шаблона BlankFragment — это только полдела. Чтобы закрепить полученные знания, нужно сделать что-то свое. Кому лень придумывать свои варианты, могут взять мой. Остальные смотрят и вдохновляются.

В наших приложениях мы используем архитектуру MVP, согласно которой каждый фрагмент это View, а любой View нужен Presenter. Оставим в стороне все, что касается Model-слоя и посчитаем классы:
  • FragmentView + ViewInterface
  • FragmentPresenter + PresenterInterface

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

Через тернии к звездам


Начнем с реализации файла template.xml. Именно его содержимое вы видите в красивых окнах при создании activity, фрагментов и т.п. Мы пойдем по самому простому пути и сделаем наш MVP-шаблон на базе существующего шаблона EmptyActivity. Копируем себе этот каталог, переименовываем в MVPActivity и приводим файл template.xml к следующему виду:

template.xml
<?xml version="1.0"?>
<template
    format="5"
    revision="1"
    name="MVP Activity"
    minApi="7"
    minBuildApi="14"
    description="Creates a new MVP activity">

    <category value="MVP" />
    <formfactor value="Mobile" />

    <parameter
        id="activityClass"
        name="Activity Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${layoutToActivity(layoutName)}"
        default="MainActivity"
        help="The name of the activity class to create" />

    <parameter
        id="generateLayout"
        name="Generate Layout File"
        type="boolean"
        default="true"
        help="If true, a layout file will be generated" />

    <parameter
        id="generateView"
        name="Generate View"
        type="boolean"
        default="true"
        help="If true, a View interface will be generated" />

   <parameter
        id="generatePresenter"
        name="Generate Presenter?"
        type="boolean"
        default="true"
        help="If true, a Presenter interface will be generated" />

    <parameter
        id="generatePresenterImpl"
        name="Generate Presenter implementation?"
        type="boolean"
        default="true"
        help="If true, a Presenter implementation will be generated" />

    <parameter
        id="layoutName"
        name="Layout Name"
        type="string"
        constraints="layout|unique|nonempty"
        suggest="${activityToLayout(activityClass)}"
        default="activity_main"
        visibility="generateLayout"
        help="The name of the layout to create for the activity" />

    <parameter
        id="isLauncher"
        name="Launcher Activity"
        type="boolean"
        default="false"
        help="If true, this activity will have a CATEGORY_LAUNCHER intent filter, making it visible in the launcher" />

    <parameter
        id="viewName"
        name="View Name"
        type="string"
        constraints="class|nonempty|unique"
        default="MainView"
        visibility="generateView"
        suggest="${underscoreToCamelCase(classToResource(activityClass))}View"
        help="The name of the View interface to create" />

    <parameter
        id="presenterName"
        name="Presenter Name"
        type="string"
        constraints="class|nonempty|unique"
        default="MainPresenter"
        visibility="generatePresenter"
        suggest="${underscoreToCamelCase(classToResource(activityClass))}Presenter"
        help="The name of the Presenter interface to create" />

    <parameter
        id="presenterImplName"
        name="Presenter Implementation Name"
        type="string"
        constraints="class|nonempty|unique"
        default="MainPresenterImpl"
        visibility="generatePresenterImpl"
        suggest="${underscoreToCamelCase(classToResource(activityClass))}PresenterImpl"
        help="The name of the presenter implementation class to create" />        
    
    <parameter
        id="packageName"
        name="Package name"
        type="string"
        constraints="package"
        default="com.mycompany.myapp" />

    <!-- 128x128 thumbnails relative to template.xml -->
    <thumbs>
        <!-- default thumbnail is required -->
        <thumb>template_blank_activity.png</thumb>
    </thumbs>

    <globals file="globals.xml.ftl" />
    <execute file="recipe.xml.ftl" />

</template>


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

    <parameter
        id="generateView"
        name="Generate View"
        type="boolean"
        default="true"
        help="If true, a View interface will be generated" />

Которые будут отображаться в виде чекбоксов, позволяющих включать/выключать генерацию тех или иных компонентов. Во-вторых, были добавлены поля для ввода имен классов и интерфейсов View и Presenter-ов:

    <parameter
        id="viewName"
        name="View Name"
        type="string"
        constraints="class|nonempty|unique"
        default="MainView"
        visibility="generateView"
        suggest="${underscoreToCamelCase(classToResource(activityClass))}View"
        help="The name of the View interface to create" />

Обязательно обратите внимание на атрибут visibility. Именно в нем прописан id-шник чекбокса, который отвечает за отображение этого поля. Основная же магия происходит в атрибуте suggest. Здесь мы убираем все возможные символы подчеркивания, отрезаем суффикс ‘Activity’ и добавляем свой суффикс ‘View’. С остальным содержимым этого файла, думаю, вопросов возникнуть не должно.

Теперь настала очередь шаблонов архитектурных компонентов. Как мы помним, располагать это все нужно в каталоге root/src. Организуем каталог следующим образом:
  • root/src
    • app_package
      • presentation
        • implementation
        • presenter
        • view

      • ui
        • activity




После этого можно заняться непосредственно шаблонными файлами. Начнем с SimpleActivity.java.ftl, который достался нам по наследству от базового шаблона. Его необходимо переместить в каталог ui/activity и привести к следующему виду:

SimpleActivity.java.ftl
package ${packageName}.ui.activity;

import ${superClassFqcn};
import android.os.Bundle;

import ${packageName}.R;
<#if generateView>import ${packageName}.presentation.view.${viewName};</#if>
<#if generatePresenter>import ${packageName}.presentation.presenter.${presenterName};</#if>
<#if generatePresenterImpl>import ${packageName}.presentation.implementation.${presenterImplName};</#if>

public class ${activityClass} extends ${superClass} <#if generateView>implements ${viewName}</#if>{
<#if generatePresenter>
    private ${presenterName} mPresenter;
</#if>
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
<#if generateLayout>
        setContentView(R.layout.${layoutName});
</#if>
<#if generatePresenterImpl>
	mPresenter = new ${presenterImplName}(this);
</#if>
    }

<#if generatePresenter>
    @Override
    protected void onDestroy() {
        mPresenter.onDestroy();
        super.onDestroy();
    }
</#if>
}


Основное отличие от исходного файла состоит в том, что добавлена реализация интерфейса View, а так же добавлено создание и уничтожение Presenter-а. Так как ранее мы сделали эти вещи опциональными, это и было отражено в коде шаблона в виде условий <#if>...</#if>.

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

SimpleView.java.ftl
package ${packageName}.presentation.view;

public interface ${viewName} {

}


SimplePresenter.java.ftl
package ${packageName}.presentation.presenter;

public interface ${presenterName} {
	
	void onDestroy();
}


SimplePresenterImpl.java.ftl
package ${packageName}.presentation.implementation;

import ${packageName}.presentation.view.${viewName};
import ${packageName}.presentation.presenter.${presenterName};

public class ${presenterImplName} implements ${presenterName} {

	private ${viewName} mView;

	public ${presenterImplName}(final ${viewName} view) {
        mView = view;
    }

    @Override
    public void onDestroy() {
        mView = null;
    }
}


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

recipe.xml.ftl
<?xml version="1.0"?>
<recipe>
    <#include "../common/recipe_manifest.xml.ftl" />

<#if generateLayout>
    <#include "../common/recipe_simple.xml.ftl" />
    <open file="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" />
</#if>

    <instantiate from="root/src/app_package/ui/activity/SimpleActivity.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/ui/activity/${activityClass}.java" />

    <open file="${escapeXmlAttribute(srcOut)}//ui/activity/${activityClass}.java" />

 <#if generateView>
    <instantiate from="root/src/app_package/presentation/view/SimpleView.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/presentation/view/${viewName}.java" />

    <open file="${escapeXmlAttribute(srcOut)}/presentation/view/${viewName}.java" />
 </#if>

  <#if generatePresenter>
    <instantiate from="root/src/app_package/presentation/presenter/SimplePresenter.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/presentation/presenter/${presenterName}.java" />

    <open file="${escapeXmlAttribute(srcOut)}/presentation/presenter/${presenterName}.java" />
 </#if>

  <#if generatePresenterImpl>
    <instantiate from="root/src/app_package/presentation/implementation/SimplePresenterImpl.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/presentation/implementation/${presenterImplName}.java" />

    <open file="${escapeXmlAttribute(srcOut)}/presentation/implementation/${presenterImplName}.java" />
 </#if>

</recipe>


В отличие от оригинального файла здесь добавляется опциональное инстанцирование наших архитектурных компонент. Файл globals.xml.ftl мы оставим без изменений, а картинку вы можете нарисовать сами или взять мою.

The End


На этом создание шаблона можно считать оконченным. Пришла пора посмотреть на результат. Для этого топаем в ANDROID_STUDIO_DIR/plugins/android/lib/templates/activities и копируем в него каталог с нашим шаблоном MVPActivity. Запускаем студию, идем в File -> New, ищем новую категорию MVP, открываем из нее наш шаблон и радуемся получившемуся результату.

И для тех, кто по какой-то причине пропустил ссылки в тексте статьи, я привожу их здесь общим списком:
+10
24.3k 178
Comments 3
Top of the day