The Setup — Customizing Android Studio with the IntelliJ Plugin SDK (Part 2)

Patches Klinefelter
Life360 Engineering
6 min readJan 26, 2022

In part 1 of this blog series, we started diving into how Android Studio plugins were used to solve real problems faced by our engineering team. Now we can start discussing the prerequisites and setup required as well as how they evolved into something the Android team could feasibly use.

Creating a New Project

Both Plugins I’ve written so far have taught me valuable lessons about how to work with the IntelliJ Platform and its expectations. I first started by following the Gradle guide to create a new project with Kotlin and Groovy:

New Project Creation

This was before the plugin projects had migrated to Kotlin DSL. You can leverage Java 11 as well, but at the time of writing Java 8 was selected since it was before Android Studio brought in better Java 11 support.

At this point, you will also want to setup your IDE to run the plugin if it’s not already done by default. Under Run -> Edit Configurations you should see or create a task that looks like this:

runIde task

A Plugin’s Structure

A plugin’s project structure is very similar to most pure Java/Kotlin projects where there is a src/main/kotlin or src/main/java directory where the Kotlin or Java plugin code will be written as well as additional source directories like test.

Project Structure

Within the structure, there are two main files that are vital and work together to define a plugin. These are:

  1. The resources/META-INF/plugin.xml file and
  2. The build.gradle or build.gradle.kts

These files contain crucial information such as what IDEs your plugin supports, the version for the plugin, changelog notes, and what additional SDK features are necessary for the plugin to function. We’ll dive into parts of these one at a time.

The build.gradle

Within the build.gradle is where additional plugins are enabled such as the IntelliJ plugin verifier (covered later), KTLint (https://github.com/pinterest/ktlint), and basic Java/Kotlin support. This is not much different from a typical Gradle plugin setup where ids and versions are defined.

plugins {
id("java")
// Kotlin support
id("org.jetbrains.kotlin.jvm") version "1.4.32"
// https://github.com/JetBrains/gradle-intellij-plugin
id("org.jetbrains.intellij") version "1.1.2"
// Kotlin linter - https://github.com/JLLeitschuh/ktlint-gradle
id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
}

Similarly, dependencies, repositories, and publication details are also defined in this file. Below is an example that defines a version and group for publishing, sets up some basic Kotlin stblib imports, and also imports JUnit as well for unit tests.

group = "com.life360.android"
version = pluginVersion
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
compileOnly(files("lib/wizard-template.jar"))
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.32")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.4.32")
testImplementation("junit:junit:4.13.1")
}

The most unique portion of a Gradle file for a plugin project is the intellij closure. This is where the version, name, additional Intellij SDK plugin features, and IDEs supported are defined. There’s also a field to set the compile-time version of the IntelliJ Platform SDK similar to how you define the compile SDK version for Android.

// See https://github.com/JetBrains/gradle-intellij-plugin/
intellij {
type.set("IC")
version.set(compileIdeVersion)
pluginName.set("Life360 Templating Plugin")
plugins.set(listOf("android"))
/**
* Download IntelliJ sources while configuring Gradle project.
*/

downloadSources.set(true)
/**
* Patch plugin.xml with since and until build
* values inferred from IDE version.
*/
updateSinceUntilBuild.set(false)
}

Another key feature is being able to test your plugin in a Sandboxed environment with the option to specify an alternate IDE. runIde handles creating a Sandboxed instance already, so there’s no additional work required there. It’s important to note that without defining an alternate IDE, the plugin will default to opening in IntelliJ Community Edition. In the example below, the location provided to the ideDir is for Android Studio. This means when runIde (sometimes called runPlugin) is executed, the plugin can be utilized and seen in a Sandboxed version of Android Studio for testing.

import org.apache.tools.ant.taskdefs.condition.Osval androidStudioPath = if (Os.isFamily(Os.FAMILY_WINDOWS)) {
"C:\\Program Files\\Android\\Android Studio"
} else {
"/Applications/Android Studio.app/Contents"
}
runIde {
ideDir.set(File(androidStudioPath))
}

Plugin Verifier

Along with manual verification, IntelliJ offers a tool (https://github.com/JetBrains/intellij-plugin-verifier) that will run compatibility checks against a list of specified versions. This is extremely useful so that it is not necessary to track down and manually test each version of the IDE you want to support. By simply running ./gradlew runPluginVerifier the tool will run a set of checks against the specified list and output potential issues to look into. It’s also possible to specify the version of plugin verifier to use.

runPluginVerifier {
verifierVersion.set("1.256")
ideVersions.set(listOf("IU-201.8743.12"))
}

The patchPluginXML closure is another place where information about the plugin is defined. It is important to know this piece of information from the docs (https://plugins.jetbrains.com/docs/intellij/gradle-guide.html#patching-the-plugin-configuration-file):

If a patchPluginXml attribute value is explicitly defined, the attribute value will be substituted in plugin.xml.

This means that regardless of values in the plugin.xml , the ones from the Gradle file will take priority. The minimum IDE version supported is defined by sinceBuild and the maximum is defined by untilBuild. You can also see in the example below that change notes can be specified.

patchPluginXml {
version.set(pluginVersion)
sinceBuild.set(minIdeVersion)
untilBuild.set(maxIdeVersion)
changeNotes.set(
"Removes documentation directories before regenerating docs, updates Kotlin, " +
"stabilizes plugin verifier version used, and updates org.jetbrains.intellij plugin"
)
}

The plugin.xml feels much like and AndroidManifest.xml . The name, id, vendor, and description are defined. It is also possible to specify the minimum and maximum supported IDE versions, but if defined in the Gradle file, they will be override

The Plugin.xml

It does seem somewhat redundant to define the name and plugins in both the intellij closure of the Gradle file and the plugin.xml but as far as I can tell, the intellij closure does not patch the plugin.xml like patchPluginXml does.

<idea-plugin>
<id>com.life360.android.templating.plugin</id>
<name>Life360 Templating</name>
<vendor>Life360</vendor>
<description>
A plugin that will add Life360 specific templates to the project creation screen of Android Studio
</description>
<idea-version since-build="201.8743.12" until-build="203.*" /> <!-- Product and plugin compatibility requirements -->
<!-- https://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html -->
<depends>org.jetbrains.android</depends>
<extensions defaultExtensionNs="com.android.tools.idea.wizard.template">
<wizardTemplateProvider implementation="com.life360.android.templating.plugin.KitAndEngineTemplateProvider" />
</extensions>
</idea-plugin>

With this setup, you should be able to see your plugin live in an IDE and begin to bring additional features to your

Plugin in the settings list

In the next entry of this series, I will cover some basics about plugins such as extension points, GUI work, and other key classes that can be used to actually start implementing your own neat features.

Additional Resources

Come join us

Life360 is the first-ever family safety membership, offering protection on the go, on the road, and online. We are on a mission to redefine how safety is delivered to families worldwide — and there is so much more to do! We’re looking for talented people to join the team: check out our jobs page.

--

--