Template plugin for Android Studio 4.1+
If you are starting a new project or in case that you want to move old one to a new architecture you should consider creating custom templates so you can avoid writing all the boilerplate code and spend that time elsewhere.
Until recently, in order to create your custom template all you needed to do is to go into $ANDROID_STUDIO/plugins/android/lib/templates/ folder and look for examples there, but starting Android Studio 4.1 that doesn’t work anymore, which I figured out right after finishing my template and wanted to update Android Studio before trying it out :)
But there is an upside to this since now you can use Kotlin instead of FTL for your template, which actually now is no longer just a template, but a JetBrains IntelliJ platform plugin instead.
First go to https://github.com/JetBrains/intellij-platform-plugin-template and follow the instructions from their README (short version: click on Use this template green button :) )
Following that wizard you will get new repository generated which is where your plugin code will be. Now you need to get it by either cloning or downloading it:
Once that is done, open it from Android Studio and we can start making some changes to make it work on your version of Android Studio.
Keep in mind that I used certain class and package names just as an example, please feel free to use your own.
Reorganize packages
For this example I’ve moved classes that were autogenerated into my base package com.github.steewsc.mvisetup:
gradle.properties
Start with gradle.properties file where you need to set:
pluginName_ to “mvi-setup”
pluginGroup to “com.github.steewsc.mvisetup”
platformVersion to match the one that you are using. For Android Studio 4.1 and 4.2 Canary 14 I set it to 2020.2, but for more details on how to determinate this checkout: https://jetbrains.org/intellij/sdk/docs/products/android_studio.html#matching-versions-of-the-intellij-platform-with-the-android-studio-version
platformPlugins to java, com.intellij.java, org.jetbrains.android, android, org.jetbrains.kotlin
plugin.xml
Next open plugin.xml file from src/main/resources/META-INF and update:
id to ”com.github.steewsc.mvisetup” (pluginGroup from gradle.properties)
name to ”mvi-setup” (pluginName_ from gradle.properties)
vendor to ”steewsc” (your name)
Then add three additional dependencies (com.intellij.modules.platform should already be there)
org.jetbrains.android
org.jetbrains.kotlin
com.intellij.modules.java
Update base package to be com.github.steewsc.mvisetup in all sections:
settings.gradle.kts
Update rootProject.name to be “mvi-setup” (or your plugin name)
Do a gradle sync and with that we conclude the setup part and we can move to code.
To get your template visible in New menu we must have:
- Class that extends WizardTemplateProvider
- Template
- Recipe
- Our template files
This is a similar setup to what we used to have before AS 4.1 except it now uses Kotlin and I actually used my old template (FTL based) as source for new plugin.
I ran into a small issue while writing plugin, regarding a Project handle/instance for my RecipeExecutor so I ended up doing it in a kinda dirty way until I find some other way (I just wanted to get it working as soon as possible :D )
Update MyProjectManagerListener.kt to store project instance:
package com.github.steewsc.mvisetup.listeners
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManagerListener
import com.github.steewsc.mvisetup.services.MyProjectService
class MyProjectManagerListener : ProjectManagerListener {
override fun projectOpened(project: Project) {
projectInstance = project
project.getService(MyProjectService::class.java)
}
override fun projectClosing(project: Project) {
projectInstance = null
super.projectClosing(project)
}
companion object {
var projectInstance: Project? = null
}
}
Also, I skipped Java version, but you can easily write it when you have this setup done.
Template Files
ActivtyAndLayout.kt
import com.android.tools.idea.wizard.template.ProjectTemplateData
import com.android.tools.idea.wizard.template.extractLetters
fun someActivity(
packageName: String,
entityName: String,
layoutName: String,
projectData: ProjectTemplateData
) = """
package $packageName
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import ${projectData.applicationPackage}.R;
class ${entityName}sActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.${extractLetters(layoutName.toLowerCase())})
}
}
"""fun someActivityLayout(
packageName: String,
entityName: String) = """
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:context="${packageName}.${entityName}sActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
"""
Recipe.kt
package other.mviSetup
import com.android.tools.idea.wizard.template.ModuleTemplateData
import com.android.tools.idea.wizard.template.RecipeExecutor
import com.android.tools.idea.wizard.template.activityToLayout
import com.android.tools.idea.wizard.template.extractLetters
import com.android.tools.idea.wizard.template.impl.activities.common.addAllKotlinDependencies
import com.github.steewsc.mvisetup.listeners.MyProjectManagerListener.Companion.projectInstance
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiManager
import someActivity
import someActivityLayoutfun RecipeExecutor.mviSetup(
moduleData: ModuleTemplateData,
packageName: String,
entityName: String,
layoutName: String
) {
val (projectData) = moduleData
val project = projectInstance ?: return
addAllKotlinDependencies(moduleData)
val virtualFiles = ProjectRootManager.getInstance(project).contentSourceRoots
val virtSrc = virtualFiles.first { it.path.contains("src") }
val virtRes = virtualFiles.first { it.path.contains("res") }
val directorySrc = PsiManager.getInstance(project).findDirectory(virtSrc)!!
val directoryRes = PsiManager.getInstance(project).findDirectory(virtRes)!!
someActivity(packageName, entityName, layoutName, projectData)
.save(directorySrc, packageName, "${entityName}sActivity.kt")
someActivityLayout(packageName, entityName)
.save(directoryRes, "layout", "${layoutName}.xml")}
fun String.save(srcDir: PsiDirectory, subDirPath: String, fileName: String) {
try {
val destDir = subDirPath.split(".").toDir(srcDir)
val psiFile = PsiFileFactory
.getInstance(srcDir.project)
.createFileFromText(fileName, KotlinLanguage.INSTANCE, this)
destDir.add(psiFile)
}catch (exc: Exception) {
exc.printStackTrace()
}
}
fun List<String>.toDir(srcDir: PsiDirectory): PsiDirectory {
var result = srcDir
forEach {
result = result.findSubdirectory(it) ?: result.createSubdirectory(it)
}
return result
}
Template.kt
package other.mviSetup
import com.android.tools.idea.wizard.template.*
import java.io.File
import mviSetup
val mviSetupTemplate
get() = template {
revision = 2
name = "MY Setup with Activity"
description = "Creates a new activity along layout file."
minApi = 16
minBuildApi = 16 category = Category.Other // Check other categories
formFactor = FormFactor.Mobile
screens = listOf(WizardUiContext.FragmentGallery, WizardUiContext.MenuEntry,
WizardUiContext.NewProject, WizardUiContext.NewModule)
val packageNameParam = defaultPackageNameParameter
val entityName = stringParameter {
name = "Entity Name"
default = "Wurst"
help = "The name of the entity class to create and use in Activity"
constraints = listOf(Constraint.NONEMPTY)
}
val layoutName = stringParameter {
name = "Layout Name"
default = "my_act"
help = "The name of the layout to create for the activity"
constraints = listOf(Constraint.LAYOUT, Constraint.UNIQUE, Constraint.NONEMPTY)
suggest = { "${activityToLayout(entityName.value.toLowerCase())}s" }
}
widgets(
TextFieldWidget(entityName),
TextFieldWidget(layoutName),
PackageNameWidget(packageNameParam)
)
recipe = { data: TemplateData ->
mviSetup(
data as ModuleTemplateData,
packageNameParam.value,
entityName.value,
layoutName.value
)
}
}
val defaultPackageNameParameter get() = stringParameter {
name = "Package name"
visible = { !isNewModule }
default = "com.mycompany.myapp"
constraints = listOf(Constraint.PACKAGE)
suggest = { packageName }
}
WizardTemplateProviderImpl.kt
package other
import com.android.tools.idea.wizard.template.Template
import com.android.tools.idea.wizard.template.WizardTemplateProvider
import other.mviSetup.mviSetupTemplate
class WizardTemplateProviderImpl : WizardTemplateProvider() {
override fun getTemplates(): List<Template> = listOf(mviSetupTemplate)
}
Now go back to plugin.xml and add your wizard template to it as:
<extensions defaultExtensionNs="com.android.tools.idea.wizard.template">
<wizardTemplateProvider implementation="other.WizardTemplateProviderImpl" />
</extensions>
Open Gradle tab and run buildPlugin
If all goes well you should see your plugin ready for installing at YOUR_PROJECT_DIR\build\libs\my-setup-0.1.0.jar
You can now either drag&drop it on Android Studio init screen or go to Settings->Plugins->Install Plugin from Disk and pick jar from libs:
Restart IDE and try your plugin by right clicking on some package in your project -> New->Other->MY Setup with Activity and if there are no errors you should see Wizard screen and after you click Next/Finish there should be new files generated by plugin.
This is just a basic New Activity setup, but you can make it work for base boilerplate setup for MVI any other pattern and make refactoring an old project a breeze:
In case you run into any issues you can see stacktrace in idea.log file (Android Studio -> Help -> Show log in Explorer/Finder..)
In case that you don’t see Activity/Fragment & other usual submenus in New menu, just uninstall a plugin (Settings->Plugins->YourPlugin -> Uninstall)
and they should reappear after IDE restart. Check idea.log for errors, correct them in plugin code, increase plugin version, build it and install again.
Demo available at:
https://github.com/steewsc/template
UPDATE #1:
I’ve just pushed an update at https://github.com/steewsc/template where it doesn’t use project instance hack.
Checkout and update androidStudioPath or androidStudioPathMacOS in gradle.properties to point to your Android Studio installation:
Also, you’ll find three run configurations:
you can use template-example [runIde] to easily build&run your plugin in AS without messing your working AS and also see a live logs from it.
UPDATE #2:
To make the plugin work with newer/yours version of AS, update:
pluginUntilBuild, pluginVerifierIdeVersions, platformCompilerVersion in gradle.properties.
Here is an example for AS Bumblebee:
UPDATE #3:
In order not to have to rebuild/install your plugin after every new AS version, I made some changes in gradle.properties and build.gradle.kts, check out the latest commit: https://github.com/steewsc/template/commit/0ec9b2dd52ac66e4f33f76994c76ccc7c87fc113#diff-c0dfa6bc7a8685217f70a860145fbdf416d449eaff052fa28352c5cec1a98c06R12
It’s also updated to use the latest version org.jetbrains.intellij 1.x