Skip to main content
This guide covers best practices and patterns for developing plugins that extend Android Code Studio’s functionality.

Plugin Architecture

Plugin Structure

A typical plugin follows this structure:
my-plugin/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/example/plugin/
│       │       ├── MyPlugin.kt
│       │       ├── MyLanguageServer.kt
│       │       ├── MyActions.kt
│       │       └── MyTemplates.kt
│       └── resources/
│           └── META-INF/
│               └── services/
│                   ├── com.tom.rv2ide.lsp.api.ILanguageServer
│                   ├── com.tom.rv2ide.templates.ITemplateProvider
│                   └── com.tom.rv2ide.actions.ActionItem
└── build.gradle.kts

Service Registration

Plugins use Java’s ServiceLoader pattern for registration. Create service files in META-INF/services/: File: META-INF/services/com.tom.rv2ide.lsp.api.ILanguageServer
com.example.plugin.MyLanguageServer
File: META-INF/services/com.tom.rv2ide.templates.ITemplateProvider
com.example.plugin.MyTemplateProvider

Creating a Language Server Plugin

Basic Language Server

Implement a language server for a custom language:
package com.example.plugin

import com.tom.rv2ide.lsp.api.*
import com.tom.rv2ide.lsp.models.*
import com.tom.rv2ide.projects.IWorkspace
import java.nio.file.Path

class MyLanguageServer : ILanguageServer {
  
  override val serverId = "my-language-server"
  override var client: ILanguageClient? = null
  
  private var workspace: IWorkspace? = null
  private val indexer = CodeIndexer()
  
  override fun shutdown() {
    indexer.clear()
    workspace = null
    client = null
  }
  
  override fun connectClient(client: ILanguageClient?) {
    this.client = client
    client?.logMessage(1, "My Language Server connected")
  }
  
  override fun setupWorkspace(workspace: IWorkspace) {
    this.workspace = workspace
    
    // Index workspace
    workspace.getSubProjects().forEach { project ->
      indexer.indexProject(project)
    }
  }
  
  override fun applySettings(settings: IServerSettings?) {
    // Apply user preferences
    settings?.let {
      configureFromSettings(it)
    }
  }
  
  override fun complete(params: CompletionParams?): CompletionResult {
    if (params == null) return CompletionResult.EMPTY
    
    val items = indexer.getCompletions(
      params.file,
      params.position,
      params.prefix
    )
    
    return CompletionResult(items)
  }
  
  override suspend fun findDefinition(
    params: DefinitionParams
  ): DefinitionResult {
    val locations = indexer.findDefinition(
      params.file,
      params.position
    )
    return DefinitionResult(locations)
  }
  
  override suspend fun findReferences(
    params: ReferenceParams
  ): ReferenceResult {
    val locations = indexer.findReferences(
      params.file,
      params.position,
      params.includeDeclaration
    )
    return ReferenceResult(locations)
  }
  
  override suspend fun signatureHelp(
    params: SignatureHelpParams
  ): SignatureHelp {
    return indexer.getSignatureHelp(
      params.file,
      params.position
    )
  }
  
  override suspend fun analyze(file: Path): DiagnosticResult {
    val diagnostics = indexer.analyze(file)
    
    // Publish to client
    client?.publishDiagnostics(file, diagnostics)
    
    return diagnostics
  }
  
  override suspend fun expandSelection(
    params: ExpandSelectionParams
  ): Range {
    return indexer.expandSelection(params.file, params.range)
  }
  
  override fun formatCode(params: FormatCodeParams?): CodeFormatResult {
    if (params == null) return CodeFormatResult(false, emptyList())
    
    val formatted = formatSource(params.content)
    val edits = createTextEdits(params.content, formatted)
    
    return CodeFormatResult(true, edits)
  }
  
  override fun handleFailure(failure: LSPFailure?): Boolean {
    if (failure != null) {
      client?.logMessage(3, "Error: ${failure.message}")
      return true
    }
    return false
  }
}

Register Language Server

Create the service registration file: File: META-INF/services/com.tom.rv2ide.lsp.api.ILanguageServer
com.example.plugin.MyLanguageServer

Creating Action Plugins

Define Custom Actions

Create actions that extend IDE functionality:
package com.example.plugin

import com.tom.rv2ide.actions.*
import com.tom.rv2ide.editor.api.IEditor
import android.graphics.drawable.Drawable

class RunWithOptionsAction : ActionItem {
  
  override val id = "plugin.run.with.options"
  override var label = "Run with Options"
  override var subtitle: String? = "Run with custom configuration"
  override var icon: Drawable? = null
  override var visible = true
  override var enabled = true
  override var requiresUIThread = false
  override var location = ActionItem.Location.EDITOR_TOOLBAR
  override val order = 200
  
  override fun prepare(data: ActionData) {
    // Enable only when project is open
    val projectManager = data.get(IProjectManager::class.java)
    enabled = projectManager?.getWorkspace() != null
  }
  
  override suspend fun execAction(data: ActionData): Any {
    // Show options dialog
    val options = showOptionsDialog(data)
    
    if (options != null) {
      // Execute run with options
      return executeRun(options)
    }
    
    return false
  }
  
  override fun postExec(data: ActionData, result: Any) {
    if (result == true) {
      showToast("Run started")
    }
  }
}

// Auto-register via service loader
class MyActionProvider : ActionItem by RunWithOptionsAction()

Action Registration

Create service file for actions: File: META-INF/services/com.tom.rv2ide.actions.ActionItem
com.example.plugin.RunWithOptionsAction
com.example.plugin.MyOtherAction

Creating Template Plugins

Custom Template Provider

Provide project and file templates:
package com.example.plugin

import com.tom.rv2ide.templates.*

class MyTemplateProvider : ITemplateProvider {
  
  private val templates = mutableListOf<Template<*>>()
  
  init {
    loadTemplates()
  }
  
  private fun loadTemplates() {
    templates.add(CustomProjectTemplate())
    templates.add(CustomFileTemplate())
    templates.add(CustomActivityTemplate())
  }
  
  override fun getTemplates() = templates.toList()
  
  override fun getTemplate(templateId: String) =
    templates.find { it.id == templateId }
  
  override fun reload() {
    templates.clear()
    loadTemplates()
  }
  
  override fun release() {
    templates.clear()
  }
}

class CustomProjectTemplate : ProjectTemplate {
  
  override val id = "custom-project"
  override val name = "Custom Project"
  override val description = "Creates a custom project structure"
  override val category = "Custom"
  override val minSdk = 21
  override val targetSdk = 34
  override val widgets = listOf(
    Widget.TextField("appName", "Application Name", "My App"),
    Widget.TextField("packageName", "Package Name", "com.example.app")
  )
  
  override suspend fun create(
    data: ProjectTemplateData,
    executor: RecipeExecutor
  ) {
    // Create project structure
    createDirectories(data, executor)
    generateFiles(data, executor)
  }
}

Plugin Initialization

Plugin Main Class

Create a main plugin class for initialization:
package com.example.plugin

import com.tom.rv2ide.actions.ActionsRegistry
import com.tom.rv2ide.lsp.api.ILanguageServerRegistry
import com.tom.rv2ide.templates.ITemplateProvider

class MyPlugin {
  
  companion object {
    private var initialized = false
    
    @JvmStatic
    fun initialize() {
      if (initialized) return
      initialized = true
      
      // Register language server
      val lspRegistry = ILanguageServerRegistry.getDefault()
      lspRegistry.register(
        MyLanguageServer(),
        listOf(".myext", ".custom")
      )
      
      // Register actions
      val actionsRegistry = ActionsRegistry.getInstance()
      actionsRegistry.registerAction(RunWithOptionsAction())
      actionsRegistry.registerAction(CustomToolAction())
      
      // Initialize templates
      ITemplateProvider.getInstance(reload = true)
      
      println("My Plugin initialized")
    }
    
    @JvmStatic
    fun shutdown() {
      // Clean up resources
      initialized = false
    }
  }
}

Build Configuration

Gradle Build File

Configure your plugin build:
// build.gradle.kts
plugins {
    kotlin("jvm")
}

dependencies {
    // Android Code Studio APIs
    compileOnly("com.tom.rv2ide:editor-api:1.0.0")
    compileOnly("com.tom.rv2ide:lsp-api:1.0.0")
    compileOnly("com.tom.rv2ide:projects:1.0.0")
    compileOnly("com.tom.rv2ide:actions:1.0.0")
    compileOnly("com.tom.rv2ide:templates-api:1.0.0")
    
    // Kotlin
    implementation(kotlin("stdlib"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    
    // Testing
    testImplementation("junit:junit:4.13.2")
    testImplementation(kotlin("test"))
}

tasks {
    jar {
        // Include service files
        from("src/main/resources")
    }
}

Testing Plugins

Unit Tests

Write tests for your plugin:
import org.junit.Test
import org.junit.Assert.*
import com.example.plugin.MyLanguageServer

class MyLanguageServerTest {
  
  @Test
  fun testServerInitialization() {
    val server = MyLanguageServer()
    assertNotNull(server.serverId)
    assertEquals("my-language-server", server.serverId)
  }
  
  @Test
  fun testCompletion() {
    val server = MyLanguageServer()
    val params = CompletionParams(
      file = Paths.get("/test/file.txt"),
      position = Position(0, 0),
      prefix = "test"
    )
    
    val result = server.complete(params)
    assertNotNull(result)
    assertTrue(result.items.isNotEmpty())
  }
}

Best Practices

Performance

Initialize resources only when needed:
private val indexer by lazy { CodeIndexer() }
private val parser by lazy { MyParser() }
Use coroutines for long operations:
override suspend fun analyze(file: Path): DiagnosticResult {
  return withContext(Dispatchers.IO) {
    // Heavy analysis work
    performAnalysis(file)
  }
}
Cache expensive computations:
private val completionCache = LRUCache<String, CompletionResult>(100)

override fun complete(params: CompletionParams): CompletionResult {
  val key = "${params.file}:${params.position}"
  return completionCache.getOrPut(key) {
    computeCompletions(params)
  }
}

Error Handling

override suspend fun execAction(data: ActionData): Any {
  return try {
    performAction(data)
  } catch (e: Exception) {
    client?.showMessage(3, "Error: ${e.message}")
    false
  }
}

override fun handleFailure(failure: LSPFailure?): Boolean {
  when (failure?.type) {
    LSPFailure.Type.TIMEOUT -> {
      client?.showMessage(2, "Operation timed out")
      return true
    }
    LSPFailure.Type.CANCELLED -> {
      // User cancelled, no message needed
      return true
    }
    else -> return false
  }
}

Resource Management

class MyLanguageServer : ILanguageServer {
  private val resources = mutableListOf<Closeable>()
  
  override fun shutdown() {
    // Clean up all resources
    resources.forEach { resource ->
      try {
        resource.close()
      } catch (e: Exception) {
        // Log but don't fail
      }
    }
    resources.clear()
  }
}

Debugging Plugins

Logging

Use the IDE’s logging system:
import com.tom.rv2ide.logging.ILogger

class MyPlugin {
  private val logger = ILogger.newInstance("MyPlugin")
  
  fun doSomething() {
    logger.debug("Starting operation")
    
    try {
      // Do work
      logger.info("Operation completed")
    } catch (e: Exception) {
      logger.error("Operation failed", e)
    }
  }
}

Client Messages

Send messages to the IDE:
// Log messages (shown in log console)
client?.logMessage(1, "Info message")
client?.logMessage(2, "Warning message")
client?.logMessage(3, "Error message")

// Show messages (shown to user)
client?.showMessage(1, "Operation successful")
client?.showMessage(2, "Warning: Consider...")
client?.showMessage(3, "Error occurred")

Distribution

Packaging

Package your plugin as a JAR:
./gradlew jar
The JAR will include:
  • Compiled classes
  • Service registration files
  • Resources

Installation

Users can install plugins by:
  1. Copying the JAR to the plugins directory
  2. Restarting Android Code Studio
  3. The plugin will be auto-loaded via ServiceLoader

Complete Example

Here’s a complete minimal plugin:
// MyPlugin.kt
package com.example.plugin

import com.tom.rv2ide.actions.*
import com.tom.rv2ide.lsp.api.*
import com.tom.rv2ide.templates.*

// Language Server
class MyLanguageServer : ILanguageServer {
  override val serverId = "my-lang"
  override var client: ILanguageClient? = null
  
  override fun shutdown() {}
  override fun connectClient(client: ILanguageClient?) { this.client = client }
  override fun applySettings(settings: IServerSettings?) {}
  override fun setupWorkspace(workspace: IWorkspace) {}
  override fun complete(params: CompletionParams?) = CompletionResult.EMPTY
  override suspend fun findReferences(params: ReferenceParams) = ReferenceResult.EMPTY
  override suspend fun findDefinition(params: DefinitionParams) = DefinitionResult.EMPTY
  override suspend fun expandSelection(params: ExpandSelectionParams) = params.range
  override suspend fun signatureHelp(params: SignatureHelpParams) = SignatureHelp.EMPTY
  override suspend fun analyze(file: Path) = DiagnosticResult.NO_UPDATE
}

// Action
class MyAction : ActionItem {
  override val id = "my.action"
  override var label = "My Action"
  override var subtitle: String? = null
  override var icon: Drawable? = null
  override var visible = true
  override var enabled = true
  override var requiresUIThread = false
  override var location = ActionItem.Location.EDITOR_TOOLBAR
  
  override fun prepare(data: ActionData) {}
  override suspend fun execAction(data: ActionData) = true
  override fun postExec(data: ActionData, result: Any) {}
}

API Overview

Complete API reference

Architecture

Understanding the IDE structure

Build docs developers (and LLMs) love