Магическая шаблонизация для Android-проектов

от автора

Начиная с Android Studio 4.1, Google прекратил поддержку кастомных FreeMarker-ных шаблонов. Теперь вы не можете просто взять и написать свои ftl-файлы и сложить их в определённую папку, чтобы Android Studio самостоятельно добавила их в меню New → Other. В качестве альтернативы нам предлагают разбираться в плагиностроении и создавать шаблоны изнутри плагинов IDEA. Нас в hh такая ситуация не очень устраивает, так как есть несколько полезных FreeMarker-ных шаблонов, которые мы постоянно используем и которые иногда нуждаются в обновлениях. Лезть в плагины, чтобы поправить какой-то шаблон? Нет уж, увольте. 

Всё это привело к тому, что мы разработали специальный плагин для Android Studio, который поможет решить эти проблемы. Встречайте – Geminio.

Про то, как работает плагин и что требуется для его настройки вы можете подробнее почитать в его README, а вот про то, как он устроен изнутри – только здесь. А ещё я расскажу, как теперь можно из плагинов создавать свои шаблоны.

*Geminio – заклинание удвоения предметов во вселенной Гарри Поттера

Немного терминологии

Чтобы меньше путаться и синхронизировать понимание того, о чём мы говорим, введём немного терминологии.

Я буду называть шаблоном набор метаданных, который необходим в построении диалога для ввода пользовательских параметров. «Рецептом» назовём набор инструкций для исполнения, который отработает после того, как пользователь введёт данные. Когда я буду говорить про шаблонный текст генерируемого кода, я буду называть это ftl-шаблонами или FreeMarker-ными шаблонами.

Чем заменили FreeMarker?

Google уже давно объявил Kotlin предпочитаемым языком для разработки под Android. Все новые библиотеки, новые приложения в Google постепенно переписываются именно на Kotlin. И плагин android-а в Android Studio не стал исключением.

Как механизм шаблонов работал до Android Studio 4.1? Вы создавали папку для описания шаблона, заводили в нём несколько файлов – globals.xml.ftl, template.xml, recipe.xml.ftl для описания параметров и инструкций выполнения шаблона, а ещё вы помещали туда ftl-шаблоны, служившие каркасом генерируемого кода. Затем все эти файлы перемещали в папку Android Studio/plugins/android/lib/templates/<category>. После запуска проекта Android Studio парсила содержимое папки /templates, добавляла в интерфейс меню New –> дополнительные action-ы, а при вызове action-а читала содержимое template.xml, строила UI и так далее.

В целом понятно, почему в Google отказались от этого механизма. Создание нового шаблона на основе FreeMarker-ных recipe-ов раньше напоминало русскую рулетку: до запуска ты никогда не мог точно сказать, правильно ли его описал, все ли требуемые параметры заполнил. А потом, по реакции Android Studio, ты пытался определить, в какой конкретной букве ошибся. Находил ошибку, менял шаблон, и всё шло на новый круг. А число шаблонов растёт, растёт и количество мест в интерфейсе, куда хочется добавлять эти шаблоны. Раньше для добавления одного и того же шаблона в несколько мест интерфейса приходилось создавать дополнительные action-ы плагины. Нужно было упрощать.

Вот так и появился удобный Kotlin DSL для описания шаблонов. Сравните два подхода:

FreeMarker-ный подход

Вот так выглядел файл template.xml:

<?xml version="1.0"?> <template     format="4"     revision="1"     name="HeadHunter BaseFragment"     description="Creates HeadHunter BaseFragment"     minApi="7"     minBuildApi="8">      <category value="HeadHunter" />      <!-- параметры фрагмента -->      <parameter         id="className"         name="Fragment Name"         type="string"         constraints="class|nonempty|unique"         default="BlankFragment"         help="The name of the fragment class to create" />      <parameter         id="fragmentName"         name="Fragment Layout Name"         type="string"         constraints="layout|nonempty|unique"         default="fragment_blank"         suggest="fragment_${classToResource(className)}"         help="The name of the layout to create" />      <parameter         id="includeFactory"         name="Include fragment factory method?"         type="boolean"         default="true"         help="Generate static fragment factory method for easy instantiation" />      <!-- доп параметры  -->      <parameter         id="includeModule"         name="Include Toothpick Module class?"         type="boolean"         default="true"         help="Generate fragment Toothpick Module for easy instantiation" />      <parameter         id="moduleName"         name="Fragment Toothpick Module"         type="string"         constraints="class|nonempty|unique"         default="BlankModule"         visibility="includeModule"         suggest="${underscoreToCamelCase(classToResource(className))}Module"         help="The name of the Fragment Toothpick Module to create" />      <thumbs>         <thumb>template_base_fragment.png</thumb>     </thumbs>      <globals file="globals.xml.ftl" />     <execute file="recipe.xml.ftl" />  </template>

А ещё был файл recipe.xml.ftl:

<?xml version="1.0"?> <recipe>      <#if useSupport>     <dependency mavenUrl="com.android.support:support-v4:19.+"/>     </#if>      <instantiate         from="res/layout/fragment_blank.xml.ftl"         to="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(fragmentName)}.xml" />      <open file="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(fragmentName)}.xml" />      <instantiate         from="src/app_package/BlankFragment.kt.ftl"         to="${srcOutRRR}/${className}.kt" />      <open file="${srcOutRRR}/${className}.kt" />      <#if includeModule>         <instantiate             from="src/app_package/BlankModule.kt.ftl"             to="${srcOutRRR}/di/${moduleName}.kt" />          <open file="${srcOutRRR}/di/${moduleName}.kt" />     </#if>  </recipe>

То же самое, но в Kotlin DSL

Сначала мы создаём описание шаблона с помощью специального TemplateBuilder-а:

val baseFragmentTemplate: Template     get() = template {         revision = 1         name = "HeadHunter BaseFragment"         description = "Creates HeadHunter BaseFragment"         minApi = 7         minBuildApi = 8          formFactor = FormFactor.Mobile         category = Category.Fragment         screens = listOf(             WizardUiContext.FragmentGallery,             WizardUiContext.MenuEntry         )          // параметры         val className = stringParameter {             name = "Fragment Name"             constraints = listOf(                 Constraint.CLASS,                 Constraint.NONEMPTY,                 Constraint.UNIQUE             )             default = "BlankFragment"             help = "The name of the fragment class to create"         }         val fragmentName = stringParameter {             name = "Fragment Layout Name"             constraints = listOf(                 Constraint.LAYOUT,                 Constraint.NONEMPTY,                 Constraint.UNIQUE             )             default = "fragment_blank"             suggest = { "fragment_${classToResource(className.value)}" }             help = "The name of the layout to create"         }         val includeFactory = booleanParameter {             name = "Include fragment factory method?"             default = true             help = "Generate static fragment factory method for easy instantiation"         }          // доп. параметры         val includeModule = booleanParameter {             name = "Include Toothpick Module class?"             default = true             help = "Generate fragment Toothpick Module for easy instantiation"         }         val moduleName = stringParameter {             name = "Fragment Toothpick Module"             constraints = listOf(                 Constraint.CLASS,                 Constraint.NONEMPTY,                 Constraint.UNIQUE             )             visible = { includeModule.value }             suggest = { "${underscoreToCamelCase(classToResource(className.value))}Module" }             help = "The name of the Fragment Toothpick Module to create"             default = "BlankFragmentModule"         }          thumb { File("template_base_fragment.png") }          recipe = { templateData ->             baseFragmentRecipe(                 moduleData = templateData as ModuleTemplateData,                 className = className.value,                 fragmentName = fragmentName.value,                 includeFactory = includeFactory.value,                 includeModule = includeModule.value,                 moduleName = moduleName.value             )         }     }

Затем описываем рецепт в отдельной функции:

fun RecipeExecutor.baseFragmentRecipe(     moduleData: ModuleTemplateData,     className: String,     fragmentName: String,     includeFactory: Boolean,     includeModule: Boolean,     moduleName: String ) {     val (projectData, srcOut, resOut, _) = moduleData      if (projectData.androidXSupport.not()) {         addDependency("com.android.support:support-v4:19.+")     }     save(getFragmentBlankLayoutText(), resOut.resolve("/layout/${fragmentName}.xml"))     open(resOut.resolve("/layout/${fragmentName}.xml"))      save(getFragmentBlankClassText(className, includeFactory), srcOut.resolve("${className}.kt"))     open(srcOut.resolve("${className}.kt"))      if (includeModule) {         save(getFragmentModuleClassText(moduleName), srcOut.resolve("/di/${moduleName}.kt"))         open(srcOut.resolve("/di/${moduleName}.kt"))     } }  private fun getFragmentBlankClassText(className: String, includeFactory: Boolean): String {     return "..." }  private fun getFragmentBlankLayoutText(): String {     return "..." }  private fun getFragmentModuleClassText(moduleName: String): String {     return "..." }

Текст шаблонов перекочевал из FreeMarker-ных ftl-файлов в Kotlin-овские строчки.

По количеству кода получается примерно то же самое, но вот наличие подсказок IDE при описании шаблона помогает не ошибаться в значениях enum-ов и функциях. Добавьте к этому валидацию при создании объекта шаблона (например, покажется исключение, если вы забыли указать один из необходимых параметров), возможность вызова шаблона из разных меню в Android Studio – и, кажется, у нас есть победитель.

Добавление шаблона через extension point

Чтобы новые шаблоны попали в существующие галереи новых объектов в Android Studio, нужно добавить созданный с помощью DSL шаблон в новую точку расширения (extension point) – WizardTemplateProvider.

Для этого мы сначала создаём класс provider-а, наследуясь от абстрактного класса WizardTemplateProvider:

class MyWizardTemplateProvider : WizardTemplateProvider() {      override fun getTemplates(): List<Template> {         return listOf(             baseFragmentTemplate         )     }  }

А затем добавляем созданный provider в качестве extension-а в plugin.xml файле:

<extensions defaultExtensionNs="com.android.tools.idea.wizard.template">     <wizardTemplateProvider implementation="ru.hh.plugins.geminio.actions.MyWizardTemplateProvider" /> </extensions>

Запустив Android Studio, мы увидим шаблон baseFragmentTemplate в меню New->Fragment и в галерее нового фрагмента.

Покажи картинки!

Вот наш шаблон в меню New -> Fragments:

А вот он же – в галерее нового фрагмента:

Если вы захотите самостоятельно пройти весь этот путь по добавлению нового шаблона из кода плагина, можете, во-первых, посмотреть на актуальный список готовых шаблонов в исходном коде Android Studio (который совсем недавно наконец-то добавили в cs.android.com), а во-вторых – почитать вот эту статью на Medium (там хорошо описана последовательность действий по созданию нового шаблона, но показан не очень правильный хак с получением инстанса Project-а – так лучше не делать).

А чем ещё можно заменить FreeMarker?

Кроме того, добавить шаблоны кода из плагинов можно с помощью File templates. Это очень просто: добавляете его в папку resources/fileTemplates и… Вы восхитительны!

А можно поподробнее?

В папку /resources/fileTemplates вашего плагина нужно добавить шаблон нужного вам кода, например, /resources/fileTemplates/Toothpick Module.kt.ft .

package ${PACKAGE_NAME}.di  import toothpick.config.Module  internal class ${NAME}: Module() {      init {             // TODO     } }

Шаблоны кода работают на движке Velocity, поэтому можно добавлять в код шаблона условия и циклы. File template-ы имеют ряд встроенных параметров, например, PACKAGE_NAME (подставит package name, в зависимости от выбранного в Project View файла), MONTH (текущий месяц) и так далее. Каждый "неизвестный" параметр будет преобразован в поле ввода для пользователя.

После запуска Android Studio в меню New вы увидите новый пункт с названием вашего шаблона:

Нажав на элемент меню, вы увидите диалог, который построился на основе шаблона.

Примеры таких шаблонов вы можете подсмотреть в репозитории MviCore коллег из Badoo. 

В чём минус таких шаблонов – они не позволяют вам одновременно добавить несколько файлов. Поэтому мы в hh их обычно не создаём.

Что не так с новым механизмом

Основная претензия к новому механизму – отсутствие возможности повлиять на ваши шаблоны извне плагинов. Вы не можете ни поменять в них текст, ни добавить новый шаблон, пока не залезете в плагин. 

Мы же хотим оперативно обновлять содержимое ftl-файлов, добавлять новые шаблоны и желательно без вмешательства в плагин, потому что отладка шаблонов из плагина — тот ещё квест =) А ещё – мы очень не хотим выбрасывать готовые шаблоны, которые заточены под использование FreeMarker-а.

Механизм рендеринга шаблонов

Почему бы не разобраться в том, как вообще происходит рендеринг новых шаблонов в Android Studio? И на основе этого механизма сделать обёртку, которая сможет пробросить созданные шаблоны на рендер.

Разобрались. Делимся. 

Чтобы заставить Android Studio построить UI и сгенерировать код на основе нужного шаблона, придётся написать довольно много кода. Допустим, вы уже создали собственный плагин, объявили зависимости от android-плагина, который лежит в Android Studio 4.1, добавили новый action, который будет отвечать за рендеринг. Тогда метод actionPerformed будет выглядеть вот так:

Обработка actionPerformed

override fun actionPerformed(e: AnActionEvent) {     val dataContext = e.dataContext      val module = LangDataKeys.MODULE.getData(dataContext)!!      var targetDirectory = CommonDataKeys.VIRTUAL_FILE.getData(dataContext)     if (targetDirectory != null && targetDirectory.isDirectory.not()) {        // If the user selected a simulated folder entry (eg "Manifests"), there will be no target directory         targetDirectory = targetDirectory.parent     }     targetDirectory!!      val facet = AndroidFacet.getInstance(module)     val moduleTemplates = facet.getModuleTemplates(targetDirectory)     assert(moduleTemplates.isNotEmpty())      val initialPackageSuggestion = facet.getPackageForPath(moduleTemplates, targetDirectory).orEmpty()      val renderModel = RenderTemplateModel.fromFacet(         facet,         initialPackageSuggestion,         moduleTemplates[0],         "MyActionCommandName",         ProjectSyncInvoker.DefaultProjectSyncInvoker(),         true,     ).apply {         newTemplate = template { ... } // build your template      }       val configureTemplateStep = ConfigureTemplateParametersStep(          model = renderModel,          title = "Template name",          templates = moduleTemplates      )       val wizard = ModelWizard.Builder()                     .addStep(configureTemplateStep).build().apply {           val resultListener = object : ModelWizard.WizardListener {           override fun onWizardFinished(result: ModelWizard.WizardResult) {               super.onWizardFinished(result)               if (result.isFinished) {                   // TODO do some stuff after creating files                   //   (renderTemplateModel.createdFiles)               }           }        }     }       val dialog = StudioWizardDialogBuilder(wizard, "Template wizard")             .setProject(e.project!!)             .build()      dialog.show() }

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

По логике программы, пользователь плагина нажимает Cmd + N на каком-то файле или package-е внутри какого-то модуля. Именно там мы и создадим пачку файлов, которые нам нужны. Поэтому необходимо определить, внутри какого же модуля и какой папки работаем.

Чтобы это сделать, воспользуемся возможностями AnActionEvent-а.

val dataContext = e.dataContext  val module = LangDataKeys.MODULE.getData(dataContext)!!  var targetDirectory = CommonDataKeys.VIRTUAL_FILE.getData(dataContext) if (targetDirectory != null && targetDirectory.isDirectory.not()) {     // If the user selected a simulated folder entry (eg "Manifests"), there will be no target directory     targetDirectory = targetDirectory.parent } targetDirectory!!

Как я уже рассказывал в своей статье с теорией плагиностроения, AnActionEvent представляет собой контекст исполнения вашего Action-а. Внутри этого класса есть свойство dataContext, из которого при помощи специальных ключей мы можем доставать нужные данные. Чтобы посмотреть, какие ещё ключи есть, обратите внимание на классы PlatformDataKeys, LangDataKeys и другие. Ключ LangDataKeys.MODULE возвращает нам текущий модуль, а CommonDataKeys.VIRTUAL_FILE – выбранный пользователем в Project View файл. Немного преобразований и мы получаем директорию, внутрь которой нужно добавлять файлы.

val facet = AndroidFacet.getInstance(module)

Чтобы двигаться дальше, нам требуется объект AndroidFacet. Facet — это, по сути, свойства модуля, которые специфичны для того или иного фреймворка. В данном случае мы получаем специфичное для Android описание нашего модуля. Из facet-а можно достать, например, package name, указанный в AndroidManifest.xml вашего android-модуля.

val moduleTemplates = facet.getModuleTemplates(targetDirectory) assert(moduleTemplates.isNotEmpty())  val initialPackageSuggestion = facet.getPackageForPath(moduleTemplates, targetDirectory).orEmpty()

Из facet-а мы достаём объект NamedModuleTemplate – контейнер для основных “путей” android-модуля: путь до папки с исходным кодом, папки с ресурсами, тестами и т.д. Благодаря этому объекту можно найти и package name для подстановки в будущие шаблоны кода.

val renderModel = RenderTemplateModel.fromFacet(     facet,     initialPackageSuggestion,     moduleTemplates[0],     "MyActionCommandName",     ProjectSyncInvoker.DefaultProjectSyncInvoker(),     true, ).apply {     newTemplate = template { ... } // build your template }

Все предыдущие элементы были нужны для того, чтобы сформировать главный компонент будущего диалога — его модель, представленную классом RenderTemplateModel. Конструктор этого класса принимает в себя:

  • AndroidFacet модуля, в котором мы создаем файлы;
  • первый предлагаемый пользователю package name (его можно будет использовать в параметрах шаблона);
  • объект, хранящий пути к основным папкам модуля, — NamedModuleTemplate;
  • строковую константу для идентификации WriteCommandAction (внутренний объект IDEA, предназначенный для операций модификации кода) – она нужна для того, чтобы у вас сработал Undo;
  • объект, отвечающий за синхронизацию проекта после создания файлов, — ProjectSyncInvoker;
  • и, наконец, флаг — true или false, — который отвечает за то, можно ли открывать все созданные файлы в редакторе кода или нет.

val configureTemplateStep = ConfigureTemplateParametersStep(     model = renderModel,     title = "Template name",     templates = moduleTemplates )  val wizard = ModelWizard.Builder()     .addStep(configureTemplateStep)     .build().apply {         val resultListener = object : ModelWizard.WizardListener {                override fun onWizardFinished(result: ModelWizard.WizardResult) {                        super.onWizardFinished(result)                        if (result.isFinished) {                                // TODO do some stuff after creating files                    //   (renderTemplateModel.createdFiles)                        }                }      } }  val dialog = StudioWizardDialogBuilder(wizard, "Template wizard")             .setProject(e.project!!)             .build() dialog.show()

Финал!

Для начала создаем ConfigureTemplateParametersStep, который прочитает переданный объект template-а и сформирует UI страницы wizard-диалога, потом пробрасываем step в модель Wizard-диалога и наконец-то показываем сам диалог.

А ещё мы добавили специальный listener на событие завершения диалога, так что после создания файлов можем ещё и как-то их модифицировать. Достучаться до созданных файлов можно через renderTemplateModel.createdFiles.

Самое сложное – позади! Мы показали диалог, который взял на себя работу по построению UI из модели шаблона и обработку рецепта внутри шаблона.

Остаётся только откуда-то получить сам шаблон. И рецепт.

Откуда взять модель шаблона

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

Мне показалось, что самый простой формат – это yaml-конфиг. Почему именно yaml? Потому что: а) выглядит проще XML, и б) внутри IDEA уже есть подключенная библиотечка для его парсинга – SnakeYaml, позволяющая в одну строчку прочитать весь файл в Map<String, Any>, который можно дальше крутить как угодно.

В данный момент конфиг шаблона выглядит так:

yaml-конфиг шаблона

requiredParams:   name: HeadHunter BaseFragment   description: Creates HeadHunter BaseFragment  optionalParams:   revision: 1   category: fragment   formFactor: mobile   constraints:     - kotlin   screens:     - fragment_gallery     - menu_entry   minApi: 7   minBuildApi: 8  widgets:   - stringParameter:       id: className       name: Fragment Name       help: The name of the fragment class to create       constraints:         - class         - nonempty         - unique       default: BlankFragment    - stringParameter:       id: fragmentName       name: Fragment Layout Name       help: The name of the layout to create       constraints:         - layout         - nonempty         - unique       default: fragment_blank       suggest: fragment_${className.classToResource()}    - booleanParameter:       id: includeFactory       name: Include fragment factory method?       help: Generate static fragment factory method for easy instantiation       default: true    - booleanParameter:       id: includeModule       name: Include Toothpick Module class?       help: Generate fragment Toothpick Module for easy instantiation       default: true    - stringParameter:       id: moduleName       name: Fragment Toothpick Module       help: The name of the Fragment Toothpick Module to create       constraints:         - class         - nonempty         - unique       default: BlankModule       visibility: ${includeModule}       suggest: ${className.classToResource().underlinesToCamelCase()}Module  recipe:   - instantiateAndOpen:       from: root/src/app_package/BlankFragment.kt.ftl       to: ${srcOut}/${className}.kt   - instantiateAndOpen:       from: root/res/layout/fragment_blank.xml.ftl       to: ${resOut}/layout/${fragmentName}.xml   - predicate:       validIf: ${includeModule}       commands:         - instantiateAndOpen:             from: root/src/app_package/BlankModule.kt.ftl             to: ${srcOut}/di/${moduleName}.kt

Вся конфигурация шаблона делится на 4 секции:

  • requiredParams – параметры, обязательные для каждого шаблона;
  • optionalParams – параметры, которые можно спокойно опустить при описании шаблона. В данный момент эти параметры ни на что не влияют, потому что мы не подключаем созданный на основе конфига шаблон через extension point.
  • widgets – набор параметров шаблона, которые зависят от пользовательского ввода. Каждый из этих параметров в конечном итоге превратится в виджет на UI диалога (textField-ы, checkbox-ы и т.п.);
  • recipe – набор инструкций, которые выполняются после того, как пользователь заполнит все параметры шаблона.

Написанный мною плагин парсит этот конфиг, конвертирует его в объект шаблона Android Studio и пробрасывает в RenderTemplateModel.

В самой конвертации практически не было ничего интересного кроме парсинга “выражений”. Я имею в виду строчки вот такого вида:

suggest: ${className.classToResource().underlinesToCamelCase()}Module

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

sealed class Command {      data class Fixed(         val value: String     ) : Command()      data class Dynamic(         val parameterId: String,         val modifiers: List<GeminioRecipeExpressionModifier>     ) : Command()      data class SrcOut(         val modifiers: List<GeminioRecipeExpressionModifier>     ) : Command()      data class ResOut(         val modifiers: List<GeminioRecipeExpressionModifier>     ) : Command()      object ReturnTrue : Command()      object ReturnFalse : Command()  }

Каждая команда знает, как себя вычислить, какой она внесёт вклад в итоговый результат, требуемый в том или ином параметре. Над парсингом выражений пришлось немного посидеть: сначала я хотел выцепить отдельные кусочки ${…} с помощью регулярок, но вы же знаете, если вы хотите решить какую-то проблему с помощью регулярных выражений, то у вас появляется ещё одна проблема. В итоге я распарсил строчку посимвольно.

Что ещё хорошо в своём собственном формате конфига – можно добавлять новые ключи и строить на них свою дополнительную логику. Так, например, появилась новая команда для рецептов – instantiateAndOpen, — которая сначала создаёт файл из текста ftl-шаблона, а потом открывает созданный файл в редакторе кода. Да-да, в FreeMarker-ных шаблонах уже были команды instantiate и open, но это были отдельные команды.

recipe:   # Можно писать вот так   - instantiate:       from: root/src/app_package/BlankFragment.kt.ftl       to: ${srcOut}/${className}.kt   - open:       file: ${srcOut}/${className}.kt    # А можно одной командой:   - instantiateAndOpen:       from: root/src/app_package/BlankFragment.kt.ftl       to: ${srcOut}/${className}.kt

Какие ещё есть плюсы в Geminio

Основной плюс – после того, как вы создали папку для шаблона с рецептом внутри, и Android Studio создала для этого шаблона Action, вы можете как угодно менять ваш рецепт и файлы с шаблонами кода. Все изменения применятся сразу же, вам не нужно будет перезапускать IDE для того, чтобы проверить шаблон. То есть цикл проверки шаблона стал в разы короче.

Если бы вы создавали шаблон из плагина, то вы бы не избежали этой проблемы с перезапуском IDE – в случае ошибки ваш шаблон бы просто не работал.

Roadmap

Я был бы рад сказать, что уже сейчас плагин поддерживает все возможности, которые были у FreeMarker-ных шаблонов, но… нет. Далеко не все возможности нужны прямо сейчас, а до некоторых мы обязательно доберёмся в рамках улучшения других плагинов. Например:

  • нет поддержки enum-параметров, которые бы отображались на UI в виде combobox-ов;
  • не все команды из FreeMarker-ных шаблонов поддерживаются в рецептах – например, нет автоматического добавления зависимостей в build.gradle, merge-а XML-ресурсов;
  • новые шаблоны страдают от той же проблемы, что и FreeMarker-ные шаблоны – нет адекватной валидации, которая бы точно сказала, где именно случилась ошибка;
  • и нет никаких подсказок IDE при описании шаблона.

Заключение

Заканчивать нужно на позитивной ноте. Поэтому вот немного позитива:

  • несмотря на то, что Google прекратил поддержку FreeMarker-ных шаблонов, мы всё равно создали инструмент для тотальной шаблонизации
  • дистрибутив плагина можно скачать в нашем репозитории;
  • я буду рад вашим вопросам и постараюсь на них ответить.

Всем успешной автоматизации.

Полезные ссылки

ссылка на оригинал статьи https://habr.com/ru/company/hh/blog/529948/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *