Русский
Русский
English
Статистика
Реклама

Freemarker

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

26.11.2020 10:08:34 | Автор: admin


Начиная с 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}.diimport toothpick.config.Moduleinternal 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.dataContextval 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 BaseFragmentoptionalParams:  revision: 1  category: fragment  formFactor: mobile  constraints:    - kotlin  screens:    - fragment_gallery    - menu_entry  minApi: 7  minBuildApi: 8widgets:  - 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()}Modulerecipe:  - 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-ных шаблонов, мы всё равно создали инструмент для тотальной шаблонизации
  • дистрибутив плагина можно скачать в нашем репозитории;
  • я буду рад вашим вопросам и постараюсь на них ответить.

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


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


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru