Android tooling · IDE plugins
Build Your Own Android Studio Plugin: Architecture, Templates and a Firebase Distribution Example
Every team has a chore that happens inside the IDE but is not part of it: send a build to testers, open the right dashboard, run a checklist. An IDE plugin turns that chore into a menu item. This article explains how plugins for Android Studio and other JetBrains IDEs are structured, shows the template you start from, and walks through one example in concept: a button that builds the app and distributes it to testers with Firebase App Distribution.
- Android Studio is the IntelliJ Platform plus the Android plugin. A plugin written for the platform runs in both, and Android-specific features need a dependency on the Android plugin.
- A plugin is a JAR with a
plugin.xmlthat declares what it adds: actions, tool windows, settings pages, services, notifications. - Start from the IntelliJ Platform Plugin Template and the IntelliJ Platform Gradle Plugin.
./gradlew runIdestarts a sandbox IDE with your plugin loaded. - Rules that matter: never block the UI thread, keep secrets in the password store, and declare the IDE versions you support.
- Example: a Distribute to Firebase action runs Gradle in the background, uploads the build through App Distribution, then shows a notification with the result.
Contents
- What a plugin is, and where Android Studio fits
- Architecture at a glance
- The project template
- What you can add: extension points
- The development loop
- Example: distribute a build to Firebase
- Where should the build run?
- Code sketches
- Threading, security and UX rules
- Compatibility, testing and publishing
- Takeaways and glossary
1. What a plugin is, and where Android Studio fits
Almost all of an IDE’s own features are plugins too: the editor, Git, Gradle, the terminal. The IntelliJ Platform provides the shared foundation (windows, editors, file model, threading, settings, a plugin loader). Products such as IntelliJ IDEA, PyCharm and Android Studio are built on top of it. Android Studio adds the Android plugin: layout editor, emulator manager, Logcat, Android Gradle Plugin support.
That gives two kinds of plugins:
- Platform plugins use only the common platform. They run in every JetBrains IDE, including Android Studio.
- Android plugins also depend on the Android plugin (
org.jetbrains.android) to use Android-specific APIs. They run in Android Studio only.
Our example only needs to run Gradle, so it could be a platform plugin. We will depend on the Android plugin anyway, to show how it is done.
2. Architecture at a glance
Three ideas explain most of the design:
- Declare, don’t hook. You list your pieces in
plugin.xml. The platform creates and calls them. Classes load only when first needed, so a plugin costs little at startup. - Services hold logic. Application services live for the whole IDE session, project services live per open project. Actions and UI stay thin and call a service.
- One UI thread. The event dispatch thread draws the interface. Slow work goes to background tasks, and results come back to the UI thread.
3. The project template
You do not start from an empty folder. JetBrains publishes the IntelliJ Platform Plugin Template, a ready-made Gradle project with a build file, CI workflows, a changelog and a sample plugin. Use it with the IntelliJ Platform Gradle Plugin, which downloads the target IDE, runs a sandbox copy, and packages, verifies and signs your plugin.
firebase-distribute/
build.gradle.kts # plugins, target IDE, dependencies, signing
gradle.properties # plugin id, version, since/until build
src/main/
kotlin/com/example/
DistributeAction.kt # menu entry
DistributeService.kt # does the work
DistributeSettings.kt # saved state + settings page
DistributeToolWindow.kt # log panel
resources/
META-INF/plugin.xml # declares everything above
messages/Bundle.properties # UI text
icons/
src/test/kotlin/ # tests
The build file names the IDE you build against. For an Android Studio plugin that means selecting Android Studio and adding the bundled Android plugin as a dependency:
plugins {
kotlin("jvm") version "<kotlin-version>"
id("org.jetbrains.intellij.platform") version "<gradle-plugin-version>"
}
repositories {
mavenCentral()
intellijPlatform { defaultRepositories() }
}
dependencies {
intellijPlatform {
androidStudio("<android-studio-version>")
bundledPlugin("org.jetbrains.android")
pluginVerifier()
zipSigner()
}
}
Version numbers change often, so the placeholders are intentional. Copy current values from the template and the Android Studio plugin guide.
4. What you can add: extension points
| You want to add… | Use | Declared as |
|---|---|---|
| A menu or toolbar command | An action (AnAction) | <action> inside a menu group |
| A panel (logs, a list, a dashboard) | A tool window | <toolWindow> |
| Options the user can change | A settings page (Configurable) and saved state | <applicationConfigurable> |
| Shared logic and data | A service | @Service or <applicationService> |
| A message after a task | A notification | <notificationGroup> |
| Reacting to events (project opened, build finished) | A listener | <projectListeners> |
| Code help (warnings, quick fixes) | Inspections and intentions | <localInspection> |
| A new way to run things | A run configuration type | <configurationType> |
The IntelliJ Platform Plugin SDK documentation lists every extension point and has a topic for each.
5. The development loop
./gradlew runIdestarts a sandbox IDE with the plugin installed. Add breakpoints in your own IDE and debug the run../gradlew buildPluginproduces a ZIP. Teammates can install it with Settings → Plugins → gear icon → Install Plugin from Disk, without publishing anywhere.
6. Example: distribute a build to Firebase
Goal: a developer chooses Build → Distribute to Firebase…, and a few minutes later testers receive the new build. The plugin builds the app, uploads it through Firebase App Distribution, and reports back. For how App Distribution itself works, see Firebase App Distribution: get test builds to testers fast.
Pieces the plugin needs
| Piece | Job |
|---|---|
| Action | The menu entry. Opens the settings if they are missing, otherwise starts the service |
| Settings page | Firebase app id, tester groups, build variant, where the service-account file is. Saved per user |
| Service | Builds the command, runs it in a background task, reads the result |
| Tool window | Shows the live Gradle output and a history of uploads |
| Notification | Success or failure, with a link to the release in the Firebase console, or to the log |
The project itself also needs the Firebase App Distribution Gradle plugin, which adds upload tasks such as appDistributionUploadRelease. Release notes and groups can come from the plugin’s settings (for example the last Git commit messages).
7. Where should the build run?
The key design choice is where the heavy work happens. The plugin is the same remote control in each case.
8. Code sketches
These sketches show the shape of each piece. Treat them as a starting point, and check the SDK documentation for the current API of the IDE version you target.
plugin.xml
<idea-plugin>
<id>com.example.firebase.distribute</id>
<name>Firebase Distribute</name>
<vendor>Example Inc.</vendor>
<depends>com.intellij.modules.platform</depends>
<depends>org.jetbrains.android</depends>
<extensions defaultExtensionNs="com.intellij">
<notificationGroup id="Firebase Distribute" displayType="BALLOON"/>
<applicationConfigurable parentId="tools" id="firebase.distribute"
instance="com.example.DistributeConfigurable" displayName="Firebase Distribute"/>
<toolWindow id="Distribution" anchor="bottom" factoryClass="com.example.DistributeToolWindow"/>
</extensions>
<actions>
<action id="Firebase.Distribute" class="com.example.DistributeAction"
text="Distribute to Firebase...">
<add-to-group group-id="BuildMenu" anchor="last"/>
</action>
</actions>
</idea-plugin>
The action: thin, it only delegates
class DistributeAction : AnAction() {
override fun getActionUpdateThread() = ActionUpdateThread.BGT
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.project != null
}
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val state = DistributeSettings.getInstance().state
if (state.appId.isBlank()) {
ShowSettingsUtil.getInstance()
.showSettingsDialog(project, DistributeConfigurable::class.java)
return
}
project.service<DistributeService>().distribute(state)
}
}
The service: background work and the result
@Service(Service.Level.PROJECT)
class DistributeService(private val project: Project) {
fun distribute(s: DistributeState) {
object : Task.Backgroundable(project, "Distributing to Firebase", true) {
override fun run(indicator: ProgressIndicator) {
val cmd = GeneralCommandLine(gradlew(project), "assembleRelease", "appDistributionUploadRelease")
.withWorkDirectory(project.basePath)
.withEnvironment("GOOGLE_APPLICATION_CREDENTIALS", s.credentialsPath)
val out = CapturingProcessHandler(cmd).runProcessWithProgressIndicator(indicator)
report(out.exitCode == 0, out.stdout + out.stderr)
}
}.queue()
}
private fun report(ok: Boolean, log: String) {
val type = if (ok) NotificationType.INFORMATION else NotificationType.ERROR
NotificationGroupManager.getInstance().getNotificationGroup("Firebase Distribute")
.createNotification(if (ok) "Build distributed to testers" else "Distribution failed", type)
.notify(project)
// also append `log` to the tool window
}
}
Settings: saved state
data class DistributeState(
var appId: String = "",
var groups: String = "qa",
var credentialsPath: String = "" // path to the service-account file, not its contents
)
@State(name = "DistributeSettings", storages = [Storage("firebase-distribute.xml")])
@Service
class DistributeSettings : PersistentStateComponent<DistributeState> {
private var state = DistributeState()
override fun getState() = state
override fun loadState(s: DistributeState) { state = s }
companion object { fun getInstance() = service<DistributeSettings>() }
}
9. Threading, security and UX rules
| Area | Rule |
|---|---|
| Threading | Never run builds, network calls or file scans on the UI thread. Use a background task with a progress indicator, support cancel, and return to the UI thread only to update the screen. Read project data inside a read action |
| Secrets | Store tokens in the IDE’s PasswordSafe, not in the XML settings file. Keep only a path to a service-account file. Never log credentials or print them in notifications |
| External processes | Build the command from separate arguments (never one concatenated string), set the working directory, and use gradlew.bat on Windows. Destroy the process if the user cancels |
| Safety | Confirm before uploading, show which variant and which groups will be used, and make a production build impossible to send by accident |
| UX | Put the action where people look for it (Build menu or toolbar), keep text short, link to details instead of dumping logs in a balloon, and keep a history in the tool window |
| Cleanup | Dispose listeners and processes when the project closes, so the plugin can be unloaded without a restart |
10. Compatibility, testing and publishing
- Compatibility. Each plugin declares the IDE builds it supports (
sinceBuildanduntilBuild). Android Studio releases follow the IntelliJ platform builds, so check which platform build your target Studio uses. The Plugin Verifier checks your plugin against several IDE versions for API breaks. - Testing. Unit-test the service with the platform’s test fixtures, and fake the process runner so tests never start a real build. Do a manual pass in the sandbox IDE for the UI.
- Distribution. Share the ZIP inside your company, host an internal plugin repository, or publish to the JetBrains Marketplace. Publishing requires a signed plugin and a review. See Publishing a plugin.
- Maintenance. Plan on updating for new IDE versions every few months. Small plugins with few dependencies are cheap to keep current.
11. Takeaways and glossary
- Android Studio is the IntelliJ Platform plus the Android plugin, so one plugin model serves all JetBrains IDEs.
- Declare pieces in
plugin.xml; keep actions thin and put logic in services. - Start from the official template, iterate with
runIde, and package with Gradle. - For “build and distribute”: run Gradle in a background task, report with a notification, keep secrets in the password store, and choose where the build runs on purpose.
| Term | In plain words |
|---|---|
| IntelliJ Platform | The shared base of JetBrains IDEs and of Android Studio. |
| Extension point | A named slot in the IDE where a plugin can add something. |
| Action | A command with a menu or toolbar entry. |
| Service | A long-lived object that holds logic or state, per IDE or per project. |
| Tool window | A dockable panel such as Logcat or Gradle. |
| Sandbox IDE | A separate IDE instance started by runIde to test the plugin safely. |
| EDT | The event dispatch thread that draws the UI and must never be blocked. |
| Plugin Verifier | A tool that checks a plugin against other IDE versions. |
Links and further reading
- IntelliJ Platform Plugin SDK · Developing plugins for Android Studio · IntelliJ Platform Gradle Plugin
- IntelliJ Platform Plugin Template · Publishing a plugin · JetBrains Marketplace
- Firebase App Distribution · Distribute with Gradle · Distribute with the Firebase CLI
- On this site: Firebase App Distribution overview · Testing Android apps