Breezing Through Android Boilerplate with Custom Activity Templates

If you’re an Android developer, chances are you’ve written a looooot of screens that show a list of something or another. Chances are you’ll also write a lot more. And my guess is that they all look pretty similar - an activity with a layout that has a RecyclerView in it. Maybe it’s wrapped in a FrameLayout or something to house a FAB. Then you have an Adapter. And that Adapter has a custom ViewHolder. And that ViewHolder references another new layout file. And you probably have a List of something being passed into the Adapter. Oh and you probably have some type of interface callback between the Activity and the Adapter. It probably has a method that sounds something like fun itemClicked(myItem: MyItem) in it.

That there boilerplate makes me sleepy just typing it out. But good news, boys and girls, we can avoid all (most…) of that by using a custom activity template.

What is an activity template?

The templates that I’m talking about are the same ones you see when you go to create a new file and choose to go through the IDE’s wizard for creating a new activity or fragment or service or whatever you want. It provides you with a little wizard where you fill in a few values and it generates a whole bunch of code, including multiple source files (which is key for our RecyclerView example). They’re even smart enough to do things like add your new activity into your AndroidManifest file. Nifty, right?

What makes up a custom template?

All of Android Studios built-in wizard-style templates can be found in (YOUR_ANDROID_STUDIO_LOCATION)/plugins/android/lib/templates. They utilize the FreeMarker language, which is a templating language used to generate HTML/Source files/emails etc. Its syntax is pretty basic: You use ${blah} to denote dynamic text that references a function or a variable. Everything else is normal static text.

Each template lives in its own folder and has 4 core components:

a template.xml file:

This is where all of the user facing components are declared - i.e. what the templates name will be, what category it’ll be in, what kind of fields it expects from the user and so on. It’s also the entrance point to the rest of your template.

a globals.xml.ftl file:

This file declares global variables that can be used throughout the template. For example - your Activity layout file name may go here so it can be reused in other components.

A root directory:

The root directory is what ultimately contains your template code - so if you’re creating an activity template this is where your ‘skeleton’ activity would go. This is where the actual Java or Kotlin code lives.

  • Note that the root directory (almost) always contains a src directory which contains an app_package directory which then ACTUALLY contains your template code. The idea is that the root directory is supposed to have a list of directories that roughly emulates your output directory, but with all that middle nonsense replaced with app_package for convenience.

And finally, the recipe.xml.ftl file:

This is the file that actually ties everything together and declares what code will be generated. The bulk of the recipe file has code blocks that look something like this:

<instantiate from="src/app_package/Adapter.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityName}Adapter.kt" /> 

Which is saying “Create a file based off the skeleton located at src/app_package/Adapter.kt.ftl (filling in any necessary information provided by the user) and place it in the users source directory with the name (whatever the user put in)Adapter.kt”

Creating a new template

Onto the meat of the subject - writing our own template. Each template lives in its own folder, so you need to create a new folder and put it in the activities directory. You can name the folder whatever you want - for the example below I named mine RecyclerviewActivity.

Now we want to create the template.xml file:

<template 
    format="4" 
    revision="1" 
    name="Recyclerview Activity"
    description="Create an activity hooked up to a recyclerview." >

    <category value="Activity" />

    <parameter 
        id="activityName" 
        name="Activity Name" 
        type="string"
        constraints="class|unique|nonempty" 
        default="BallerFeature" 
        help="Name of your new Activity" />

    <globals file="globals.xml.ftl" /> 
    <execute file="recipe.xml.ftl" />

</template> 

Most of this is pretty self explanatory - the name/description params in the root template tag will be what the user sees when they open the wizard. The category determines what bucket the wizard is placed in.

The interesting piece is the parameter value - this is what the user will actually be asked to fill in. You can provide defaults and help text and all that jazz. You can even add constraints like “Must be a valid Java class identifier” or “Must be a unique class name”. The id value is how you can refer to the value in the rest of the template.

The last two lines declare where our global variables live, and finally tell the template wizard to run the recipe file, which we’ll create later on.

Next we want to create our globals.xml.ftl file (the one we referenced above):

<?xml version="1.0"?>
<globals>
    <global id="resOut" value="${resDir}" />
    <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
    <global id="listItem" value="${classToResource(activityName)}_list_item" />
    <global id="adapter" value="${activityName}Adapter" /> 
    <global id="viewholder" value="${activityName}ViewHolder" /> 
    <global id="activityLayout" value="activity_${classToResource(activityName)}" />
     <!-- These values are all necessary to utilize the manifest merging code
      included below -->
    <#include "../common/common_globals.xml.ftl" /> 
    <global id="parentActivityClass" value=""/>
    <global id="excludeMenu" type="boolean" value="true" /> 
    <global id="generateActivityTitle" type="boolean" value="false" /> 
    <global id="hasNoActionBar" type="boolean" value="false" /> 
    <global id="isLauncher" type="boolean" value="false" />
    <global id="activityClass" value="${activityName}Activity" /> 
</globals>

As previously mentioned, these variables can be referenced throughout the template. We’re declaring several variables that will be used throughout our skeleton files - the name of our list item layout, the adapter class and so on. They utilize the activityName we got from the user above.

The <#include /> line is being used to include a bunch of common globals in the activities folder - we don’t actually care about any of those values, but to get manifest merging to work properly we need to include them. The same goes for the rest of the variables below that line.

Next up on the chopping block is our actual skeleton files - these go in the new root/src/app_package/ directory.

First we create our recyclerview adapter skeleton Adapter.kt.ftl:

package ${packageName}

import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

class ${adapter}: RecyclerView.Adapter<${adapter}.${viewholder}>() {

  override fun onBindViewHolder(holder: ${activityName}ViewHolder?, position: Int) {
    TODO("not implemented")
  }

  override fun getItemCount(): Int {
    TODO("not implemented")
  }

  override fun onCreateViewHolder(parent: ViewGroup, 
    viewType: Int): ${viewholder} {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.${listItem}, parent, false)
    return ${viewholder}(view)
  }

  class ${viewholder}(root: View): RecyclerView.ViewHolder(root)

  interface  Callback {
    fun itemClicked()
  }
}

It’s utilizing the adapter variable we declared above in the globals.xml.ftl file as the class name, and the listItem value we declared in our globals.xml.ftl file as the name for the layout file used by the ViewHolder. It’s also utilizing the viewholder variable simlarly declared in our globals file to name the ViewHolder class.

Next we create our list item layout skeleton List_Item.xml.ftl:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello, World!"/>

</LinearLayout>

Nothing too crazy going on there.

Next up is our Activity layout skeleton, Activity_Layout.xml.ftl:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <android.support.v7.widget.RecyclerView
      android:id="@+id/list"
      android:layout_width="match_parent"
      android:layout_height="match_parent" />

</FrameLayout>

Pretty boring.

Last up is our Activity skeleton, Activity.kt.ftl:

package ${packageName}

import android.os.Bundle
import android.support.v7.app.AppCompatActivity

class ${activityName}Activity: AppCompatActivity(), ${adapter}.Callback {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.${activityLayout})
  }

  override fun itemClicked() {
    TODO("not implemented")
  }
}

It also utilizes the activityName we got from the user. It implements the Callback we declared in the Adapter.kt.ftl skeleton and sets the contentView to the layout file declared in Activity_Layout.xml.ftl.

Finally, to tie it all together, we create the recipe.xml.ftl file:

<?xml version="1.0"?>
<recipe>
    <#include "../common/recipe_manifest.xml.ftl" />

    <instantiate from="src/app_package/Activity.kt.ftl"
                 to="${escapeXmlAttribute(srcOut)}/${activityName}Activity.kt" />

    <instantiate from="src/app_package/Activity_Layout.xml.ftl"
                 to="${escapeXmlAttribute(resOut)}/layout/${activityLayout}.xml"/>

    <instantiate from="src/app_package/List_Item.xml.ftl"
                 to="${escapeXmlAttribute(resOut)}/layout/${listItem}.xml" />

    <instantiate from="src/app_package/Adapter.kt.ftl"
                 to="${escapeXmlAttribute(srcOut)}/${adapter}.kt" />

    <open file="${srcOut}/${activityName}Activity.kt"/>
    <open file="${resOut}/layout/${activityLayout}.xml" />
    <open file="${srcOut}/${adapter}.kt" />
    <open file="${resOut}/layout/${listItem}.xml" />
</recipe>

It instantiates four files - first it utilizes the Activity.kt.ftl skeleton file to create the actual activity implementation. The newly created file is placed wherever srcOut points to and is named activityNameActivity - so whatever the user input as the activity name followed by Activity.

It then does the same instantiation dance for Activity_layout.xml.ftl, List_Item.xml.ftl, and Adapter.kt.ftl

But before it instantiates any of the above classes, there’s an <#include /> line. That’s how we get our newly created activity to be added to the AndroidManifest file. The included file utilizes the <merge /> tag under the hood to create a new AndroidManifest file and merge it into the existing one. Luckily this code is all included in the common directory so we don’t need to write any of it ourselves. 👍 to laziness. This merging code is why we needed all those extra params in the globals.xml.ftl file - it expects those globals to be present and throws a series of (brutally) opaque errors if it can’t find them.

And that’s it! If you navigate to File -> New -> Activity you should see Recyclerview Activity as an option. Boilerplate generated!

You can find some (admittedly old) documentation here.