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

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


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


  • Исходный код Geminio и его дистрибутив
  • Исходный код Android Studio и код актуальных шаблонов
  • Статья от RedMadRobot про FreeMarker-ные шаблоны
  • Статья на Medium про добавление собственных шаблонов изнутри плагинов
Источник: https://habr.com/ru/company/hh/blog/529948/


Интересные статьи

Интересные статьи

Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционн...
На работе я занимаюсь поддержкой пользователей и обслуживанием коробочной версии CRM Битрикс24, в том числе и написанием бизнес-процессов. Нужно отметить, что на самом деле я не «чист...
Однажды, в понедельник, мне пришла в голову мысль — "а покопаюсь ка я в новом ядре" (новым относительно, но об этом позже). Мысль не появилась на ровном месте, а предпосылками для нее стали: ...
В 2019 году люди знакомятся с брендом, выбирают и, что самое главное, ПОКУПАЮТ через интернет. Сегодня практически у любого бизнеса есть свой сайт — от личных блогов, зарабатывающих на рекламе, до инт...
«Битрикс» — кошмар на костылях. Эта популярная характеристика системы среди разработчиков и продвиженцев ныне утратила свою актуальность.