SUPERCHARGE YOUR WORKFLOW: A STEP-BY-STEP GUIDE TO ADVANCED FILE TEMPLATES

Efficient Code Organization with File Templates: Strategies for Code Factorization and Consistency

Unveiling File Template Magic: A Practical Guide to Structured File Templates

Marc Picone
9 min readAug 14, 2023

--

Introduction

Welcome back to our series on making Android development easier with IntelliJ file templates. We’ve covered a lot so far — creating files, automating structures — and now we’re diving into an essential topic: how to keep your code organized in templates.

Have you ever felt overwhelmed when small changes in your code’s names make you update lots of templates?

We’ve got your back. In this article, we’ll explore how to solve this challenge. We’ll revisit what we’ve learned about the parse function and include tab, and show you how to use them to make your code cleaner and simpler. You’ll see how to easily handle common tasks like string manipulation.

So, let’s jump in! We’ll help you get your code templates organized and reusable, so you can focus on what you do best — crafting awesome Android apps!

Link

Articles:

Gists:

Before We Begin

Now that we’re comfortable using the parse function to reveal specific values or functions, let’s pause and reflect. Up until now, our include file templates have been all about generating content that we can directly use within the template.

But here’s the thing: the parse function is essentially a function that carries out the code within the provided template. So, if we make the include template simply assign values without actually printing anything, what happens next?

This approach opens up an interesting possibility. We can parse templates that don’t output anything directly, yet we can still access all the values defined within. It’s a subtle shift in perspective, and the results are quite remarkable!

1.1 Introducing the UtilsStringFormat Template

Our first template is dedicated to making your life easier by centralizing string manipulation tasks, providing a more efficient approach when crafting various file templates.

As you review the following code, take note of the “INIT” section’s significance, a detail we’ll unravel in part 2.2 of this article.

## ****************************** HOW TO USE ********************************************
## This template is used to help manipulating String in your template creation
##
## To use this template use the parse directive on top of your template :
## `#parse("UtilsStringFormat.kt")`
##
## Then you can access all the following :
##
## **************** NAME to transform the variable `$NAME` *******************************
##
## NameToCamelCase -> transform the variable `$NAME` from `snake case` to `camel case`
## usage -> directly call the reference : `${NameToCamelCase}`
##
## NameToUpperCase -> transform the variable `$NAME` to `upper case`
## usage -> directly call the reference : `${NameToUpperCase}`
##
## NameToLowerCamelCase -> transform the variable `$NAME` from `snake case` to `lower camel case`
## usage -> directly call the reference : `${NameToLowerCamelCase}`
##
## NameToSnakeCase -> transform the variable `$NAME` from `camel case` to `snake case`
## usage -> directly call the reference : `${NameToSnakeCase}`
##
## -------------------------------------------------------------------------------------
## ****** MACRO to transform the variable `$Arg` that will be passed in argument *******
##
## ToArray -> split the $Arg String on the $Delimiter
## usage -> 1. Call the macro : `#ToArray ( $my_list $my_delimiter)`
## // the value passed in the list could be either reference `$my_ref`
## // or String `"my_element1, my_element2" ","`
## 2. Use the reference to print or use the result array `${ToArray}`
##
## ToLowerCamelCase -> transform the $Arg String in `lower camel case` accept either `snake case`
## or `camel_case` format for the $Arg
## usage -> 1. Call the macro : `#ToLowerCamelCase ( $my_string )`
## // the value passed in the list could be either reference `$my_ref`
## // or String `"my_element1"`
## 2. Use the reference to print or use the value `${ToLowerCamelCase}`
##
## ToUpperCamelCase -> transform the $Arg String in `upper camel case` accept either `snake case`
## or `lower camel case` format for the $Arg
## usage -> 1. Call the macro : `#ToUpperCamelCase ( $my_string )`
## // the value passed in the list could be either reference `$my_ref`
## // or String `"my_element1"`
## 2. Use the reference to print or use the value `${ToUpperCamelCase}`
##
## ToSnakeCase -> transform the $Arg String in `snake case` accept either `camel case`
## or `lower camel case` format for the $Arg
## usage -> 1. Call the macro : `#ToSnakeCase ( $my_string )`
## // the value passed in the list could be either reference `$my_ref`
## // or String `"my_element1"`
## 2. Use the reference to print or use the value `${ToSnakeCase}`
##
## ------------------------------------ INIT -------------------------------------------
## If `$NAME` does not exist assign `$FEATURE_NAME` value to `$NAME` in order to prompt it.
## This happen when we use a reference for the path of a parent template
#if (${NAME} && ${NAME} != "")
## // Nothimg to do, the $NAME has been prompt
#else
#set($NAME = $FEATURE_NAME)
#end
##
## ---------------------------- NAME TRANSFORMATION --------------------------------------
#set( $TrimName = ${StringUtils.collapseSpaces(${NAME})})
## ----------------- NameToCamelCase
#set($NameToCamelCase = ${StringUtils.removeAndHump(${TrimName}, "_")})
##
## ----------------- NameToUpperCase
#set($NameToUpperCase = $TrimName.toUpperCase())
##
## ----------------- NameToLowerCamelCase
#set($firstLetter = $NameToCamelCase.substring(0,1).toLowerCase())
#set($theRest = $NameToCamelCase.substring(1))
#set($NameToLowerCamelCase = ${firstLetter} + ${theRest})
##
## ----------------- NameToSnakeCase
#set( $regex = "([a-z])([A-Z]+)")
#set( $replacement = "$1_$2")
#set( $NameToSnakeCase = $TrimName.replaceAll($regex, $replacement).toLowerCase())
##
## ------------------------------- MACRO --------------------------------------
## ----------------- ToArray
#set($Arg = "")
#set($Delimiter = "")
#macro( ToArray $Arg $Delimiter )
#set($ToArray = ${StringUtils.split($Arg, $Delimiter)})
#end
##
## ----------------- ToLowerCamelCase
#set($Arg = "")
#macro ( ToLowerCamelCase $Arg )
#set($modifiedName = ${StringUtils.removeAndHump(${Arg}, "_")})
#set($firstLetter = $modifiedName.substring(0,1).toLowerCase())
#set($theRest = $modifiedName.substring(1))
#set($ToLowerCamelCase = ${firstLetter} + ${theRest})
#set( $ToLowerCamelCase = ${StringUtils.collapseSpaces(${ToLowerCamelCase})})
#end
##
## ----------------- ToUpperCamelCase
#set($Arg = "")
#macro( ToUpperCamelCase $Arg)
#set($ToUpperCamelCase = ${StringUtils.removeAndHump(${Arg}, "_")})
#set( $ToUpperCamelCase = ${StringUtils.collapseSpaces(${ToUpperCamelCase})})
#end
##
## ----------------- ToSnakeCase
#set($Arg = "")
#macro( ToSnakeCase $Arg)
#set( $regex = "([a-z])([A-Z]+)")
#set( $replacement = "$1_$2")
#set( $ToSnakeCase = $Arg.replaceAll($regex, $replacement).toLowerCase())
#set( $ToSnakeCase = ${StringUtils.collapseSpaces(${ToSnakeCase})})
#end

While engaging with your code, be mindful of spaces and line breaks. As templates are designed for printing, even spaces and line breaks are printed, potentially affecting path formatting in unexpected ways.

Thanks to this approach, you now have a single location for modifications — the include template.

Let’s explore applying the same logic to create a template for path formatting and optimize the organization of your code.

1.2 Introducing the UtilsPathFormat Template

Our next template is dedicated to streamlining path formatting, simplifying the access to various common paths within your project.

## ****************************** HOW TO USE ********************************************
## This template is used to have easy access to different common path.
##
## To use this template use the parse directive on top of your template :
## `#parse("UtilsPathFormat.kt")`
##
## Then you can access all the following :
## ****************************** PATH ********************************************
## ---------------------------- ROOT PATH
#set($folder = "")
#set($foreach = "")
#set($RootPath = "")
#set($endRootPath = "..")
#foreach ($folder in ${StringUtils.split(${PACKAGE_NAME},".")})
#set($RootPath = "$!RootPath../")
#if ($foreach.hasNext == false)
#set($RootPath = "$!RootPath$endRootPath")
#end
#end
## ---------------------------- FOLDER
#set($folder = "")
#set($foreach = "")
#foreach ($folder in ${StringUtils.split(${PACKAGE_NAME},".")})
#if ($foreach.hasNext == false)
#set($Folder = ${folder})
#end
#end
## ---------------------------- FOLDER TO CAMEL CASE
#set($folder = "")
#set($foreach = "")
#set($FolderToCamelCase = "")
#foreach ($folder in ${StringUtils.split(${PACKAGE_NAME},".")})
#if ($foreach.hasNext == false)
#set($FolderToCamelCase = ${StringUtils.removeAndHump($folder,"_")})
#end
#end
## ---------------------------- COMMOM PATH
#set($ResPath = "${RootPath}/res")
#set($AndroidTestPath = "${RootPath}/../androidTest/java/com")
#set($TestPath = "${RootPath}/../test/java/com")

Really cool, now we could use this value in every new file template we create. With this powerful tool at your disposal, you’re ready to embrace enhanced efficiency and organization in your template-based development process.

Let’s dive into the practical aspects of integrating these templates into your Android development workflow.

2.1 Accessing Template Values

To start utilizing these values, you can seamlessly integrate them into your file templates.

Begin by opening a new file template in the IDE. In the template body, initiate the parsing of the required template file using the #parse directive. This simple step grants you access to all the values defined in the template.

Exemple :

#parse("UtilsStringFormat.kt")
#set($VIEW_NAME = ${NameToCamelCase})
class ${VIEW_NAME} @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
//
}

In this example, we’ve used the NameToCamelCase value from the UtilsStringFormat template to define the format for the view name. By parsing the UtilsStringFormat file, we gain access to this transformation.

But often, a single template might not suffice, and several child templates could require the same value. Similar to how we handled the Utils template, we can create an include file to encapsulate the state of your entire template. This approach ensures consistent value usage throughout the template.

By adopting this method, any modifications to the value can be made in one central location, simplifying the process of applying changes across the entire template.

Now, let’s put these principles into action with a straightforward example of creating a custom view template. This example will highlight the benefits of integrating template values in your daily workflow.

2.2 Leveraging Template State with Includes

To illustrate this concept, let’s consider an example where we want to create a CustomViewState include file to hold and manage the state of our custom view template.

Begin by generating a new file using the IDE’s include tab. We’ll name the file CustomViewState and assign the .kt extension.

Example :

#parse("UtilsStringFormat.kt")
#parse("UtilsPathFormat.kt")
##
## Package ---------------------------------------------
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
#set($package = "package ${PACKAGE_NAME}.${NAME}
")
#end
## Imports ---------------------------------------------
#set($viewImports = "import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.IdRes
import ${PACKAGE_NAME}.R
")
##
## *************** EXPOSED VALUES *************************
#set($VIEW_IMPORTS = ${viewImports})
#set($PACKAGE = ${package})
#set($VIEW_NAME = ${NameToCamelCase})
#set($PRESENTER_NAME = "${NameToCamelCase}Presenter")
#set($CONTRACT_NAME = "${NameToCamelCase}Contract")
#set($CONTRACT_SCREEN_NAME = "${CONTRACT_NAME}.Screen")
#set($CONTRACT_USER_ACTION_NAME = "${CONTRACT_NAME}.UserAction")
## ******************** PATH **********************************
#set($VIEW_PATH = "${NAME}/${VIEW_NAME}")
#set($CONTRACT_PATH = "${NAME}/${CONTRACT_NAME}")
#set($PRESENTER_PATH = "${NAME}/${PRESENTER_NAME}")
#set($LAYOUT_PATH = "${ResPath}/${NAME}/layout/${NAME}")

To maintain clarity and precision, I have adopted a naming convention using upper snake case for values within the template. This strategy enhances readability and helps avoid errors, especially since templates lack linting functionality.

Now that we have our state holder CustomViewState prepared, let's put it to use and begin crafting our CustomView in the Files tab.

View Details (Parent template):

  • Name: CustomView
  • Extension: kt
  • File Name: #parse("CustomViewState.kt")${VIEW_PATH}

Note :

A quick pause to address a slight nuance, while employing the parse function in the file name is possible, it does necessitate a minor adjustment in the UtilsStringFormat.kt template:

## ------------------------------------ INIT -------------------------------------------
## If `$NAME` does not exist assign `$FEATURE_NAME` value to `$NAME` in order to prompt it.
## This happen when we use a reference for the path of a parent template
#if (${NAME} && ${NAME} != "")
## // Nothimg to do, the $NAME has been prompt
#else
#set($NAME = $FEATURE_NAME)
#end
##

This INIT section ensures that if the $NAME variable isn't explicitly defined, the value of $FEATURE_NAME is assigned to it. This covers cases where references to parent template paths are involved. Now, let's continue exploring our example scenario:

  • Body :
#parse("CustomViewState.kt")
${PACKAGE}
${VIEW_IMPORTS}
class ${VIEW_NAME} @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

private val view = inflate(context, R.layout.${NAME}, this)
private val userAction by lazy { createUserAction() }

override fun onAttachedToWindow() {
super.onAttachedToWindow()
userAction.onAttachedToWindow()
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
userAction.onDetachedFromWindow()
}

private fun createScreen() = object : ${CONTRACT_SCREEN_NAME} {
}

private fun createUserAction(): ${CONTRACT_USER_ACTION_NAME} {
if (isInEditMode) {
return object : ${CONTRACT_USER_ACTION_NAME} {
override fun onAttachedToWindow() {}
override fun onDetachedFromWindow() {}
}
}
return ${PRESENTER_NAME}(
createScreen()
)
}
}

Presenter Details (Child template):

  • File Name: #parse("CustomViewState.kt")${PRESENTER_PATH}
  • Extention : kt

Body :

#parse("CustomViewState.kt")
${PACKAGE}
class ${PRESENTER_NAME}(
private val screen : ${CONTRACT_SCREEN_NAME}
) : ${CONTRACT_USER_ACTION_NAME} {

override fun onAttachedToWindow(){

}

override fun onDetachedFromWindow(){

}
}

Contract Details (Child Template):

  • File name : #parse("CustomViewState.kt")${CONTRACT_PATH}
  • Extension: kt
  • Body :
#parse("CustomViewState.kt")
${PACKAGE}
interface ${CONTRACT_NAME} {

interface UserAction {

fun onAttachedToWindow()

fun onDetachedFromWindow()
}

interface Screen {

}
}

Layout Details (Child Template):

  • File Name : #parse("CustomViewState.kt")${LAYOUT_PATH}
  • Extention : kt
  • Body :
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="android.widget.FrameLayout">



</merge>

And that’s it, with this we could use our file template to quickly create a MVP structure like we done before. But now our template is much more easy to maintain!

Conclusion:

In this article, we’ve harnessed the full potential of advanced file templates. By wielding the parse function’s magic, we’ve seamlessly integrated values and functions, fueling our templates with adaptability and precision.

The introduction of UtilsStringFormat and UtilsPathFormat has changed our string manipulation and path formatting, offering a streamlined and organized approach. With include templates at our side, we’ve centralized value management, ensuring consistency and effortless modifications.

As we wrap up this journey, remember that this is just one facet of a multi-dimensional exploration. If you’re hungry for more template magic, venture further into the series. Discover the intricacies of MVP architectures and delve into the world of Room database templates. The possibilities are vast, limited only by your creativity.

Happy coding, and may your templates continue to light your path toward exceptional app creation! 🚀

--

--

Marc Picone

Hello! I'm Marc, Android developer at MWM. I've been creating innovative music, drawing, and creative apps with MWM since 2021. Let's connect and exchange ideas