Android tooling · IDE plugins

Build Your Own Android Studio Plugin: Architecture, Templates and a Firebase Distribution Example

Khairul Alam·October 2026·~18 min read
Android StudioIntelliJ PlatformPluginsKotlinFirebase

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.

The whole post in 60 seconds
  • 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.xml that 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 runIde starts 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
  1. What a plugin is, and where Android Studio fits
  2. Architecture at a glance
  3. The project template
  4. What you can add: extension points
  5. The development loop
  6. Example: distribute a build to Firebase
  7. Where should the build run?
  8. Code sketches
  9. Threading, security and UX rules
  10. Compatibility, testing and publishing
  11. 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:

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

Your plugin (a JAR, described by plugin.xml) Actionsmenu and toolbar entries Tool windowpanel with output Settings pageapp id, groups Serviceslogic and saved state Background tasksprogress, cancel Notificationsballoons with links plugin.xml: extension points IntelliJ Platform (+ the Android plugin in Android Studio) extension points · services · project model · UI toolkit · threading rules Gradle, ADB, shellrun as child processes FirebaseApp Distribution Git, web APIsnotes, CI triggers
The plugin never owns the IDE. It declares extensions, and the platform calls them at the right moment.

Three ideas explain most of the design:

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…UseDeclared as
A menu or toolbar commandAn action (AnAction)<action> inside a menu group
A panel (logs, a list, a dashboard)A tool window<toolWindow>
Options the user can changeA settings page (Configurable) and saved state<applicationConfigurable>
Shared logic and dataA service@Service or <applicationService>
A message after a taskA 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 thingsA run configuration type<configurationType>

The IntelliJ Platform Plugin SDK documentation lists every extension point and has a topic for each.

5. The development loop

CodeKotlin, XML RunrunIde Testtest, verifier PackagebuildPlugin SignsignPlugin PublishMarketplace repeat until it works release steps
The sandbox IDE has its own settings and config folder, so your experiments never touch your real IDE.

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.

Developer Plugin Gradle Firebase Build > Distribute validate settings run in backgroundassemble + upload upload build release link exit code + log notification success: release linkfailure: log link testers getan email orapp invite
The developer waits on a progress bar in the corner of the IDE, not on a frozen window.

Pieces the plugin needs

PieceJob
ActionThe menu entry. Opens the settings if they are missing, otherwise starts the service
Settings pageFirebase app id, tester groups, build variant, where the service-account file is. Saved per user
ServiceBuilds the command, runs it in a background task, reads the result
Tool windowShows the live Gradle output and a history of uploads
NotificationSuccess 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.

A. Local Gradle plugin runs ./gradlewon the dev machine + simple, works offline+ uses local signing − ties up the laptop− “works on my machine” B. Trigger CI plugin calls the CI API,CI builds and uploads + same build every time+ secrets stay in CI − needs CI access− slower feedback C. Direct upload plugin uploads a buildthat already exists + fastest+ tiny plugin − you must build first− easy to send a stale file
Small teams start with A. Teams with a CI system usually settle on B.

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

AreaRule
ThreadingNever 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
SecretsStore 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 processesBuild 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
SafetyConfirm before uploading, show which variant and which groups will be used, and make a production build impossible to send by accident
UXPut 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
CleanupDispose listeners and processes when the project closes, so the plugin can be unloaded without a restart

10. Compatibility, testing and publishing

11. Takeaways and glossary

TermIn plain words
IntelliJ PlatformThe shared base of JetBrains IDEs and of Android Studio.
Extension pointA named slot in the IDE where a plugin can add something.
ActionA command with a menu or toolbar entry.
ServiceA long-lived object that holds logic or state, per IDE or per project.
Tool windowA dockable panel such as Logcat or Gradle.
Sandbox IDEA separate IDE instance started by runIde to test the plugin safely.
EDTThe event dispatch thread that draws the UI and must never be blocked.
Plugin VerifierA tool that checks a plugin against other IDE versions.
← Back to all writing