Magic Templating for Android Projects

Pavel Strelchenko
hh.ru
Published in
11 min readNov 30, 2020

Starting with Android Studio 4.1, Google ended its support for custom FreeMarker templates. Now you can’t just write your FTL files and put them in a specific folder so that Android Studio can add them to the “New → Other” menu. As an alternative, we are invited to learn plugin building and create templates from within IDEA plugins. In hh, we are not particularly happy with this situation, as there is a number of useful FreeMarker templates that we constantly use and which need to be updated once in a while. Delving into the plugins to fix some template? Give me a break, please.

All this led us to develop a special plugin for Android Studio that will help to address these issues. And now meet Geminio

You can learn more details on how this plugin works and what is required to configure it in the README file, but only here can you explore its inner mechanics. In addition, I will describe how the plugins could be used to create your own templates now.

*Geminio is a spell used to duplicate an object in the Harry Potter universe.

A Bit of Terminology

To reduce confusion and synchronize the understanding of what we’re discussed here, we will introduce a bit of terminology.

When using the term “template,” I’m referring to a set of metadata that is needed to build a dialog for entering user parameters. “Recipe” will mean a set of instructions to be executed after the user enters data. When I refer to the template text of generated code, I will use the terms “FTL templates” or “FreeMarker templates.”

What Replaced FreeMarker?

Google has long declared Kotlin its preferred language for Android development. All new libraries and new apps in Google are gradually being rewritten in Kotlin. So the android plugin in Android Studio was no exception.

How did template engine work prior to Android Studio 4.1? You would create a folder for the template description, add several files, such as globals.xml.ftl, template.xml, and recipe.xml.ftl, to describe parameters and instructions for executing a template, and, in addition, you would also place the FTL templates there to serve as a framework for generated code. Then all these files would be put to the folder “Android Studio/plugins/android/lib/templates/<category>. Once the project was started, Android Studio would parse the contents of “/templates” folder, add more actions to the “New ->” menu interface, and when an action was called, Android Studio would read the contents of “template.xml,” build UI, etc.

In general, it is clear why Google has abandoned this engine. Creating a new template with FreeMarker recipes used to be like Russian roulette; before starting it, you never knew for sure whether you described it properly or whether you filled in all the required parameters. Then, based on the response from Android Studio, you tried to figure out which particular character was used in error. After finding that error, you would change the template, and every step had to be repeated in a new cycle. As the number of templates was growing, so was the number of areas in the interface where you would want to add these templates. Previously, you had to create additional action plugins to add the same template to several areas in the interface. It all had to be simplified.

This is how a user-friendly Kotlin DSL came into existence to describe the templates. Let's compare the two approaches:

  • FreeMarker's approach

First of all, we need to create template.xml for describing our parameters:

template.xml with parameters

And then we create recipe.xml.ftl to describe actions we need to do with all parameters:

recipe.xml.ftl with commands

Then we need to put all of our FTL templates into a special folder, but it is one more story to be told.

  • The same thing but in Kotlin DSL

First things first — we need to create a template description with TemplateBuilder:

baseFragmentTemplate.kt with template description

Then we create the recipe in a separate function:

Recipe — extension function for RecipeExecutor

You see, the text of the templates migrated from the FreeMarker FTL files to the Kotlin strings.

When you look at the amount of code, it is about the same in both cases, but the availability of IDE tooltips when describing a template helps you to avoid errors in enum values ​​and functions. Add to this the validation when creating a template object (for example, an exception will be displayed if you forget to specify a required parameter), the ability to call the template from different menus in Android Studio — and it looks like we’ve got a winner.

Adding a Template via an Extension Point

To make sure that new templates are included in the existing galleries of new objects in Android Studio, you need to add the template created with DSL to “WizardTemplateProvider,” a new extension point.

To do this, we first create a class that inherits from this WizardTemplateProvider interface:

Next, add the created provider as an extension to the plugin.xml file:

After running Android Studio, we will see the baseFragmentTemplate in the New -> Fragment menu and in the gallery of the new fragment.

Here’s our template in New -> Fragments menu:

And here it is in the gallery of the new fragment:

If you want to perform on your own the entire procedure of adding a new template from the plugin code, you can, first, look at the current list of existing templates in the Android Studio source code (which was only recently added to cs.android.com). Secondly, read this article on Medium (it gives a good description of the sequence of steps to create a new template but offers a rather inaccurate hack for obtaining a Project instance — it would be better not to use it).

What Else Could Replace FreeMarker?

Alternatively, you can add code templates from plugins using File templates. It’s very simple — add it to the folder “resources/fileTemplates” (e.g. /resources/fileTemplates/Toothpick Module.kt.ft) and… you’re great!

These templates work with the Velocity engine, so you can add some conditions or loops into your template. Also, File templates have several embedded parameters you can use, e.g., PACKAGE_NAME (it will add a package name, depending on a selected file in Project View), MONTH (current month, may be useful for your docs), etc. Every unknown parameter would be converted into separate edit text in the template's dialog.

After running Android Studio, a new item with the name of your template will be displayed in the “New” menu:

Clicking this menu item will display a dialog built with the template.

You can find examples of such templates in the MviCore repository created by our colleagues at Badoo.

What is the downside of such templates? You can’t use them to add multiple files at the same time. This is why we at hh usually avoid creating them.

What’s Wrong with the New Kotlin DSL Engine

The main objection regarding the new engine is the fact that you can’t influence your templates any way other than from the plugins. Without getting into the IDEA's plugin, you can neither change the text in existing templates nor add a new template.

But we want to promptly update the content of the FTL files, add new templates, and preferably without messing with the plugin because debugging templates from a plugin is one hell of a mission =) In addition, we really do not want to throw away existing templates that have been customized for using FreeMarker.

Template Rendering Engine

How about exploring the rendering of new templates in Android Studio? And then use this engine to make a wrapper capable of forwarding created templates for rendering.

We actually did this. And now, we want to share it with you.

Getting Android Studio to build the UI and generate code based on the desired template requires a lot of newly written code. Let’s assume that you have already created your own plugin, declared dependencies on the android plugin, and added a new AnAction that will take charge of rendering. In this case, the actionPerformed method will be as follows:

Wow, that’s quite a lot of code! On the other hand, it relieves us of the need to think about building dialogs with different parameters, working with code generation, and much more, so now let’s figure this out.

The logic of the program requires the plugin user to press “Cmd + N” for some file or package within a module. This is where we will create a bunch of the files we’ll need. So, it is necessary to determine within which module and folder we are working.

To do this, let’s use the features of AnActionEvent.

As I already mentioned in my article about plugin-building theory, AnActionEvent is a context for running your Action. This class has the dataContext property, which we can use with special keys to extract the desired data. To view the remaining keys, look at such classes as PlatformDataKeys, LangDataKeys, and others. The LangDataKeys.MODULE key returns the current module, while CommonDataKeys.VIRTUAL_FILE returns the file selected by the user in Project View. After a few transformations, we get a directory where we can add files.

To move forward, we need an AndroidFacet object. In essence, a Facet is a framework-specific module property. In this case, we generate the Android-specific description of our module. You can use a facet to extract the package name specified in AndroidManifest.xml of your android module.

We will use facet to extract the NamedModuleTemplate object, a container for the key paths of the android module, such as the path to the source code folder, resource folder, test folder, etc. With this object, you can find the package name to be inserted in future code templates.

All previous items were needed in order to build the key component of the future dialog — its model represented by the RenderTemplateModel class. The designer of this class includes:

  • AndroidFacet of the module where we create the files;
  • the first package name prompted to the user (you can use it in the template parameters);
  • NamedModuleTemplate, an object that stores paths to the main folders of the module;
  • a string constant to identify WriteCommandAction (an internal IDEA object for code modification operations) — it is needed for the proper operation of the ‘Undo’ command;
  • ProjectSyncInvoker, an object that takes charge of synchronizing the project once the files are created;
  • and, finally, a true” or “false” flag defining whether or not all generated files can be opened in the code editor.

And, now, the final part!

First, we create ConfigureTemplateParametersStep, which will read the transferred template object and generate the UI of the Wizard dialog page, then we move the step into the Wizard dialog model, and, finally, display the dialog itself.

In addition, we added a special listener triggered by the dialog end event, so that after creating the files, we can also modify them. You can get to created files by using renderTemplateModel.createdFiles.

Once you have done this, the hardest part is over! We showed a dialog that took charge of building the UI from the template model and processing the recipe within the template.

All that remains now is to find out where we can get the template. And the recipe, too.

Where You Can Get the Template Model

My initial objective was to enable colleagues to store templates as separate resources rather than in the form of code. So I needed some kind of intermediate data format which I could later convert into the data required by Android Studio for building a dialog.

I thought that the simplest format would be YAML config. Why YAML? Because it: a) looks simpler than XML; and b) IDEA already includes a little library for parsing it named SnakeYaml that allows the reading of the entire file into Map<String, Any> in one line of code. This Map can be tinkered with any way you want.

At this point, the template config looks as follows:

The entire template configuration can be divided into 4 sections:

  • requiredParams are parameters required for each template;
  • optionalParams are parameters that you can safely omit when describing a template. Currently, these parameters do not affect anything, because we are not using the extension point to connect any template based on the config.
  • widgets is a set of template parameters that depend on user input. Each of these parameters will eventually turn into a widget based on dialog UI (textField, checkbox, etc.);
  • recipe is a set of instructions executed after the user has filled in all of the template parameters.

The plugin that I wrote can parse this config, convert it into an Android Studio template object, and send to RenderTemplateModel.

There was virtually nothing particularly interesting in the conversion process, other than parsing the expressions. Here, I refer to the strings like this:

It was necessary to read this string and understand whether it included the use of any variables, and whether these variables were modified in any way. I couldn’t figure out anything better than parsing such expressions into a sequence of commands:

Each command knows how to calculate itself, how it will contribute to the final outcome required for a parameter. I had to wrestle a bit over the parsing of expressions — first, I wanted to extract some little ${…} parts with regular expressions but, as you know, when you want to use regular expressions to solve one problem, you suddenly discover that you also have to deal with another one. In the end, I parsed the string character by character.

Another benefit of using your own config format is that you can add new keys to build your additional logic. This is how instantiateAndOpen was developed, a new command for recipes which, first, creates a file from the text of FTL template, and then opens the created file in the code editor. Of course, FreeMarker templates already had instantiate and open commands, but these were separate commands.

Other Benefits of Geminio

The key benefit is the ability to change your recipe and files just the way you like with code templates after you created a folder for a template with a recipe in it, and Android Studio has generated an Action template for this. All changes will be applied immediately, and there is no need to restart the IDE in order to validate the template. In other words, the template validation cycle has been reduced manyfold.

If you were creating a template from a plugin, then you would have to face the issue of restarting the IDE, and, in case of an error, your template would simply not work.

Roadmap

I would be happy to announce that the plugin currently supports all features of FreeMarker-based templates, but… no. The vast majority of the features are not needed right now, and we will definitely get to some of them as part of the improvement of other plugins. For example:

  • there is no support for the enum parameters that would be displayed in UI as comboboxes;
  • some commands from FreeMarker templates are not supported in recipes, for example, there is no automatic addition of dependencies to build.gradle, or merge from XML resources;
  • new templates suffer from the same issue as FreeMarker templates, as there is no adequate validation that would identify exactly where the error occurred;
  • and there are no IDE tooltips when describing a template.

Conclusion

An article should end on a positive note. So here’s a bit of positive side:

  • although Google stopped supporting FreeMarker templates, we created a complete templating tool;
  • you can download the plugin distribution kit from our repository;
  • I will be happy to receive your questions and do my best to answer them.

I wish you all successful automation.

Useful Links

--

--

Pavel Strelchenko
hh.ru
Editor for

Android developer at hh.ru, recent Mobius speaker, passionate about IntelliJ IDEA plugins development