В процессе создания новых приложений очень часто приходится писать однотипный код и воспроизводить уже проверенные временем структуры классов. IDE (в частности Android Studio) облегчает во многом задачи, связанные с созданием новых компонентов для приложения. Список шаблонных Aсtivity растет с каждой новой версией SDK Tools. Но что если нужно создать свой собственный шаблон? Для одного файла это легко, но что делать, если нужно создать шаблон с созданием нескольких файлов? Зная возможности IDE, это вполне реализуемо, ведь Activity создается вместе с файлом разметки, да и фрагмент к ней добавить можно. Однако описание такой функциональности найдено не было, потому пришлось искать решение для данной возможности самостоятельно.
Под катом хочу рассказать вам о том, как эта возможность реализуется в Android Studio.
Первой мыслью было спросить у коллег, пользующихся продуктами JetBrains, как они решают эту проблему. Ответ был весьма прост. Для создания структуры классов был написан отдельный скрипт, который выполнял все необходимые действия.
Действительно, такое решение было первым, которое приходило на ум. И у него есть свое преимущества, а именно, — независимость от IDE. Однако это неудобно, и вопрос совместимости с IDE оставался актуальным.
В исходниках SDK есть описание способа решить поставленную задачу. Android IDE Template Format.
Получив такой инструмент я сразу решил его опробовать и этим опытом спешу поделиться.
Для создания шаблонов используется FreeMarker. Это очень похоже на использование шаблонов PHP и Django.
Суть заключается в том, что у нас есть определенный шаблон и форма для ввода параметров. После ввода, параметры передаются в шаблон, и на основе этих данных собирается необходимый класс.
Список всех используемых шаблонов находится по следующему адресу
android-studio-folderpluginsandroidlibtemplates
Для создания своего шаблона добавляем в директорию activities(или other) новый католог со следующим списком файлов
Обязательными являются только файлы template.xml и recipe.xml.
template.xml
Этот XML-файл содержит метаданные о шаблоне, в том числе имя, описание, категорию и видимые пользователю параметры, доступные в IDE в качестве опций.
<!-- Это шаблон пустой Activity. Используйте в шаблоне параметр format=4, как указано в этом шаблоне. -->
<template
format="4"
revision="2"
minApi="7"
minBuildApi="16"
name="Blank Activity"
description="Creates a new blank activity, with navigation.">
<!-- Строковой параметр; значение доступно для FreeMarker в обрабатываемых файлах (.ftl files)
как ${activityName}. -->
<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="layoutName"
name="Layout Name"
type="string"
constraints="layout|unique|nonempty"
suggest="${activityToLayout(activityClass)}"
default="activity_main"
help="The name of the layout to create for the activity" />
<parameter
id="navType"
name="Navigation Type"
type="enum"
default="none"
help="The type of navigation to use for the activity">
<option id="none">None</option>
<option id="tabs" minApi="11">Tabs</option>
<option id="pager" minApi="11">Swipe Views</option>
<option id="dropdown" minApi="11">Dropdown</option>
</parameter>
<parameter
id="fragmentName"
name="Fragment Name"
type="string"
constraints="class|unique|nonempty"
default="MainFragment"
visibility="navType != 'none'"
help="The name of the fragment class to create" />
<!-- 512x512 PNG миниатюра. -->
<thumbs>
<!-- Стандартная миниатюра. -->
<thumb>template_default.png</thumb>
<!-- Значение меняется в зависимости от выбранных параметров -->
<thumb navType="tabs">template_tabs.png</thumb>
<thumb navType="dropdown">template_dropdown.png</thumb>
</thumbs>
<!-- Указание глобальных переменных -->
<globals file="globals.xml.ftl" />
<!-- Инструкция (скрипт) для запуска при создании экземпляра
шаблон. -->
<execute file="recipe.xml.ftl" />
</template>
Элемент template
format — версия шаблона, которой этот шаблон придерживается. Должно быть 4. (ну вот так вот)
revision — необязательный параметр. Версия этого шаблона (которую можно увеличить при обновлении шаблона)
name — отображаемое имя шаблона.
description — описание шаблона.
minApi — необязательный параметр. Проверяется минимальный уровень API, для этого шаблона. IDE перед созданием шаблона будет проверять, что minSdkVersion не ниже этого значения.
Элемент parameter
Определяет настраиваемые пользователем параметр шаблона.
id — идентификатор для переменной. Доступен в файле глобальной переменной. Если идентификатор Foo, то значение параметра будет доступно в FreeMarker файлах как ${Foo}.
name — отображаемое имя параметра.
type — тип параметра. Доступные значения string, boolean, enum, и separator.
constraints — необязательный. Ограничения параметра. Ограничения могут быть объединены с помощью |.
- nonempty — значение не должно быть пустым
- apilevel — указывает уровень API
- package — значение должно представлять собой допустимое имя пакета Java
- app_package — значение должно представлять собой допустимое имя Android app package
- module — значение должно представлять собой допустимое имя модуля
- class — значение должно представлять собой допустимое Java класса
- activity — значение должно представлять собой полное имя класса Activity
- layout — значение должно представлять собой допустимое имя layout ресурса
- drawable — значение должно представлять собой допустимое имя drawable ресурса
- string — значение должно представлять собой допустимое имя string ресурса
- id — значение должно представлять собой допустимое имя id
- unique — значение должно быть уникальным
- exists — значение должно существовать
suggest — необязательный. Автоматически обновляемые выражения, зависящие от значения других параметров.
default — необязательный. Значение по умолчанию для этого параметра.
visibility — необязательный. FreeMarker выражение, определяющее, видимость компонента.
help — необязательный. Подсказка для этого параметра.
Элемент option
Для параметров типа enum, представляет собой элементы для выбора.
id — значение параметра устанавливается, если выбрана эта опция.
minApi — необязательный. Требуется минимальный уровень API.
[text] — Текст отображаемый при выборе этого параметра.
Элемент thumb
Показывает миниатюру для шаблона. Елементы должны быть внутри элемента . Текст содержащийся в этом элементе это путь к миниатюре. Если этот элемент имеет атрибуты, они будут рассматриваться как селектор.
Пример:
<thumbs>
<thumb>template.png</thumb>
<thumb navType="tabs">template_tabs.png</thumb>
</thumbs>
В качестве миниатюры будет показана template_tabs.png, если в navType выбрано значение tabs.
globals.xml.ftl
XML-файл содержит глобальные переменные для использования в рабочих макетах FreeMarker.
Пример globals.xml.ftl:
<globals>
<global id="srcOut"
value="src/${slashedPackageName(packageName)}" />
<global id="activityNameLower"
value="${activityName?lower_case}" />
<global id="activityClass"
value="${activityName}Activity" />
</globals>
recipe.xml.ftl
Этот XML-файл содержит отдельные инструкции, которые должны быть выполнены при генерации кода для этого шаблона.
Например, вы можете скопировать некоторые файлы или каталоги(инструкция copy), возможно создание файлов через FreeMarker (инструкция instantiate), и можно попросить IDE открыть файл (инструкция open).
Примечание: Имя файла инструкций определяется в template.xml. Однако по соглашению, лучше называть его recipe.xml.ftl.
Примечание: Глобальные переменные globals.xml.ftl доступны для использования в recipe.xml.ftl.
<recipe>
<#if appCompat?has_content>
<dependency mavenUrl="com.android.support:appcompat-v7:+"/>
</#if>
<merge from="AndroidManifest.xml.ftl"
to="${escapeXmlAttribute(manifestDir)}/AndroidManifest.xml" />
<!-- простое копирование файлов, без запуска FreeMarker -->
<copy from="res/drawable-mdpi"
to="${escapeXmlAttribute(resDir)}/res/drawable-mdpi" />
<copy from="res/drawable-hdpi"
to="${escapeXmlAttribute(resDir)}/res/drawable-hdpi" />
<copy from="res/drawable-xhdpi"
to="${escapeXmlAttribute(resDir)}/res/drawable-xhdpi" />
<copy from="res/drawable-xxhdpi"
to="${escapeXmlAttribute(resDir)}/res/drawable-xxhdpi" />
<copy from="res/menu/main.xml"
to="${escapeXmlAttribute(resDir)}/res/menu/${activityNameLower}.xml" />
<!-- запуск FreeMarker и merge с уже существующими файлами -->
<merge from="res/values/dimens.xml"
to="${escapeXmlAttribute(resDir)}/res/values/dimens.xml" />
<merge from="res/values-large/dimens.xml"
to="${escapeXmlAttribute(resDir)}/res/values-large/dimens.xml" />
<merge from="res/values/styles.xml"
to="${escapeXmlAttribute(resDir)}/res/values/styles.xml" />
<merge from="res/values/strings.xml.ftl"
to="${escapeXmlAttribute(resDir)}/res/values/strings.xml" />
<!-- выбор ресурсов для добавления -->
<#if navType?contains("pager")>
<instantiate
from="${escapeXmlAttribute(resDir)}/res/layout/activity_pager.xml.ftl"
to="${escapeXmlAttribute(resDir)}/res/layout/activity_${activityNameLower}.xml" />
<#elseif navType == "tabs" || navType == "dropdown">
<copy from="${escapeXmlAttribute(resDir)}/res/layout/activity_fragment_container.xml"
to="${escapeXmlAttribute(resDir)}/res/layout/activity_${activityNameLower}.xml" />
<#else>
<copy from="${escapeXmlAttribute(resDir)}/res/layout/activity_simple.xml"
to="${escapeXmlAttribute(resDir)}/res/layout/activity_${activityNameLower}.xml" />
</#if>
<!-- выбор Activity для добавления-->
<#if navType == "none">
<instantiate from="src/app_package/SimpleActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<#elseif navType == "pager">
<instantiate from="src/app_package/PagerActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<#elseif navType == "tabs">
<instantiate from="src/app_package/TabsActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<#elseif navType == "dropdown">
<instantiate from="src/app_package/DropdownActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
</#if>
<!-- открыть ресурс и Java class после добавления -->
<open file="${escapeXmlAttribute(resDir)}/res/layout/${activityNameLower}.xml" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
</recipe>
Инструкции поддерживают следующие методы:
dependency
Указывает, что шаблону необходима библиотека. Если ее нет, IDE добавит зависимость в проект.
Атрибут mavenUrl
Указание maven пакета библиотеки. Для примера, com.android.support:appcompat-v7:+
copy
Единственный обязательный параметр это путь к исходным файлам для копирования относительно каталога root/. Все необходимые подкаталоги создаются автоматически, при необходимости.
Каталог создания по умолчанию совпадает с расположение шаблона относительно папки /root. Есть необязательный параметр для указания пути создания файла. Обратите внимание, что расширение .ftl автоматически убирается. Для примера запись /> является правильной. Будет создан файл с именем strings.xml, а не strings.xml.ftl.
instantiate
Тоже самое что и copy, но для исходного файла сперва будет запущен FreeMarker.
merge
Эта инструкция будет запускать FreeMarker для исходного файла, а затем объединять содержимое с уже существующим файлом в проекте или создавать новый файл. Наиболее распространенным примером использования этой инструкции является добавление компонентов к файлу AndroidManifest.xml или объединение таких ресурсов, как strings.xml.
open
Открывает файл, указанный в аргументах, после завершения генерации кода.
mkdir
Проверяет существование каталога, указанного в аргументах
root/
Все файлы шаблонов (resources, Java sources, Android Manifest) должны располагаться в директории root/
Однако вместо размещения исходные файлов в src/com/google/… можно просто использовать src/app_package/.
Built-in Template Functions
Существует несколько функций, не входящих в список функций FreeMarker, которые можно использовать.
Вот некоторые из них:
string activityToLayout(string)
Преобразует имя класса Activity в строку, подходящую для использования в качестве имени ресурса. Например, FooActivity будет преобразовано к activity_foo.
string camelCaseToUnderscore(string)
Преобразует camel-case строку в строку с подчеркиванием. Например, FooBar будет преобразовано к foo_bar.
string classToResource(string)
Преобразует Android имя класса, например, FooActivity или FooFragment, к соответствующему ресурсу с добавлением суффикса 'Activity' или 'Fragment'.
На данный момент распознаются:
- Activity
- Fragment
- Provider
- Service
string layoutToActivity(string)
Преобразует ресурс с суффиксом в строку класса. Например activity_foo будет переведено к Java классу FooActivity.
string slashedPackageName(string)
Преобразует полное имя пакета Java к соответствующему пути к каталогу. Например, если данный аргумент com.example.foo, то возвращаемое значение будет com/example/foo.
string underscoreToCamelCase(string)
Преобразует подчеркивания строки к соответствующей camel case строке. Например, foo_bar к FooBar.
Built-in Template Parameters
Некоторые параметры, доступные для FreeMarker выражений и файлов.
packageName
Имя пакета проекта, например, com.example.foo
isNewProject
Логическое значение указывающее является ли шаблон шаблоном нового проекта.
manifestDir
Каталог с AndroidManifest.xml.
srcDir
Каталог исходных файлов Java для проекта.
resDir
Каталог корневой директории ресурсов (res/) для проекта.
За полным списком функций и параметров обращайтесь к документации.
В целом, создание собственного шаблона не является чем-то трудным. Потратив некоторое время на его создание, можно избавиться рутины по созданию компонентов и структур в проекте.
Пользуйтесь и автоматизируйте все, что можно автоматизировать.
Полезные ссылки
Документация
Статья о создании собственного Activity для Google Analytics
Работа с FreeMarker
Автор: Witgelm