Когда собаке программисту нечего делать, он начинает все автоматизировать. Мне по роду своей деятельности приходится писать много кода и, конечно, хочется какие-то повторяющие вещи обобщить в виде библиотек, скриптов или шаблонов для 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, открываем из нее наш шаблон и радуемся получившемуся результату.

И для тех, кто по какой-то причине пропустил ссылки в тексте статьи, я привожу их здесь общим списком:

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


  1. Yoto
    12.01.2016 19:27
    +1

    Вопрос не столько по теме статьи, сколько по MVP.
    Скажите, а почему Вы в шаблон SimplePresenter.java.ftl не включили методы onResume/onPause? У меня, например, почти каждый презентер нуждается в них.


    1. Fi5t
      12.01.2016 22:59
      +5

      Пример больше учебный и призван показать основной принцип. Поэтому вы можете построить свой так, как считаете более верным. Мнений о том как «правильно варить MVP» существует великое множество и поэтому конечно приведенный выше пример может быть как раскритикован так и одобрен разными программистами.