Skip to content

Latest commit

 

History

201 Commits

Folders and files

Repository files navigation

droid.nvim

Android development workflow for Neovim. Build, run, and debug Android apps without leaving your editor.

Requirements

  • Neovim 0.11+
  • Android SDK. droid finds adb, emulator and avdmanager inside it
  • gradlew in project root
  • Java 11+ (only for the Groovy language server. kotlin-lsp ships its own)
  • scrcpy (optional, for device mirroring)
  • android CLI (optional, used by default when installed; adds :DroidScreenshot, :DroidLayout, :DroidDocs, :DroidCreate and :DroidSdk, and handles emulators and install-and-launch)
  • Android Studio Quail 2+ (optional, running with the project open; adds :DroidLint, :DroidDeclaration, :DroidUsages and :DroidVersions)

SDK Environment Setup

Android Studio handles this automatically, but if you're using Neovim without it, you need to set these manually.

Linux / macOS

Add to your shell profile (~/.bashrc, ~/.zshrc, etc.):

export ANDROID_HOME="$HOME/Android/Sdk"
export ANDROID_AVD_HOME="$HOME/.config/.android/avd"
export PATH="$ANDROID_HOME/emulator:$PATH"
export PATH="$ANDROID_HOME/platform-tools:$PATH"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"

Windows

Add to your system environment variables:

setx ANDROID_HOME "%LOCALAPPDATA%\Android\Sdk"
setx ANDROID_AVD_HOME "%USERPROFILE%\.android\avd"
setx PATH "%ANDROID_HOME%\emulator;%ANDROID_HOME%\platform-tools;%ANDROID_HOME%\cmdline-tools\latest\bin;%PATH%"

droid finds the SDK from android.android_home in its config, then vim.g.android_sdk, then $ANDROID_SDK_ROOT or $ANDROID_HOME, then the usual install locations. It passes that SDK to android-cli, and to Gradle when the environment sets no ANDROID_HOME, so they all work on the same one.

Troubleshooting

Run :checkhealth droid when a command does not work. It reports on Neovim, the project (Gradle wrapper, SDK location for Gradle, applicationId), android-cli, the SDK tools, running devices and the language servers, with advice for each problem it finds.

Installation

-- lazy.nvim (recommended: pin to a specific version)
{
  "rizukirr/droid-nvim",
  ft = { "kotlin", "groovy", "xml" },
  opts = {},
}

droid-nvim is dependency-free. Two optional plugins improve the experience:

  • mason-org/mason.nvim — recommended, so droid can offer to install the Kotlin and Groovy language servers. Without it, put the servers on your $PATH or set $KOTLIN_LSP_DIR.
  • nvim-treesitter/nvim-treesitter — optional; richer syntax highlighting and more accurate :DroidKdoc signature parsing. Install the parsers with :TSInstall kotlin groovy. Without it, droid falls back to Neovim's bundled kotlin/groovy syntax and a regex parser.

Important

droid-nvim manages the Kotlin and Groovy LSPs internally. If you have other plugins configuring these LSPs (e.g., nvim-lspconfig), consider disabling them to avoid conflicts.

Selection UI (optional)

droid-nvim's device/emulator/variant pickers go through vim.ui.select. With no override installed, Neovim falls back to a numbered cmdline prompt (inputlist). To get a floating picker, install any of the following — droid-nvim picks it up automatically with zero extra config:

Plugin Notes
telescope-ui-select.nvim Routes vim.ui.select through Telescope. Best if you already use Telescope.
snacks.nvim (snacks.picker) Modern, fast, actively maintained.
fzf-lua Register with require("fzf-lua").register_ui_select().
mini.pick Lightweight, zero deps. Use MiniPick.ui_select.

Example with telescope-ui-select:

{
  "nvim-telescope/telescope-ui-select.nvim",
  dependencies = { "nvim-telescope/telescope.nvim" },
  event = "VeryLazy",
  config = function()
    require("telescope").load_extension("ui-select")
  end,
}

Configuration

All options are optional. Defaults shown below:

require("droid").setup({
    lsp = {
        enabled = true,                    -- Master toggle for all LSPs
        jre_path = nil,                    -- Shared JRE path (auto-detected)
        folding = true,                    -- fold by the server's foldingRange

        -- Kotlin LSP (kotlin-lsp)
        kotlin = {
            enabled = true,
            jdk_for_symbol_resolution = nil,
            jvm_args = {},                 -- ignored by kotlin-lsp (uses bundled launcher)
            root_markers = nil,
            import_progress = "progress",  -- "progress" (LSP progress) or "off"
            auto_reload = true,            -- reload the workspace when a build file is saved
            suppress_diagnostics = {},     -- e.g. { "PackageDirectoryMismatch" }
            -- Hide a code only on declarations carrying one of the annotations
            suppress_when_annotated = { FunctionName = { "Composable" } },
            inlay_hints = {
                enabled = true,
                parameters = true,
                parameters_compiled = true,
                parameters_excluded = false,
                types_property = true,
                types_variable = true,
                function_return = true,
                function_parameter = true,
                lambda_return = true,
                lambda_receivers_parameters = true,
                value_ranges = true,
                kotlin_time = true,
                call_chains = false,
            },
        },

        -- Groovy LSP (groovy-language-server)
        groovy = {
            enabled = true,
            root_markers = nil,            -- defaults: build.gradle, settings.gradle
        },
    },
    logcat = {
        mode = "horizontal",               -- "horizontal" | "vertical" | "float"
        height = 15,                       -- for "horizontal"
        width = 80,                        -- for "vertical"
        float_width = 120,                 -- for "float"
        float_height = 30,
        max_lines = 5000,                  -- keep this many lines; 0 keeps all
        filters = {
            package = "mine",              -- "mine" (auto-detect), a package, or "none"
            log_level = "v",               -- v, d, i, w, e, f
            tag = nil,                     -- show only this tag
            grep_pattern = nil,            -- keep only lines containing this text
        },
    },
    android = {
        android_home = nil,                -- override ANDROID_HOME env var
        android_avd_home = nil,            -- override ANDROID_AVD_HOME env var
        auto_select_single_target = true,  -- use the only running device without asking
        logcat_startup_delay_ms = 2000,    -- wait after launch before opening logcat
    },
    -- android-cli backend. "auto" uses the `android` binary if on PATH,
    -- true forces it (warns when missing), false disables it entirely.
    -- When active, droid-nvim routes emulator management, :DroidRun
    -- deploy, screenshots, and KB docs through android-cli.
    android_cli = "auto",
})

LSP Support

droid.nvim provides complete LSP support for Android development:

Language LSP Server Install offered Java
Kotlin kotlin-lsp Yes (Mason) Bundled with the server
Groovy groovy-language-server Yes (Mason) 11+

Each LSP starts lazily when you first open a file of that type. If it is not installed, droid.nvim offers to install it via Mason, once per session, and starts it when the install finishes.

LSP Detection Order

For each LSP, droid.nvim searches in this order:

  1. Mason — ~/.local/share/nvim/mason/packages/{lsp-name}/
  2. Environment variable — $KOTLIN_LSP_DIR or $GROOVY_LSP_DIR
  3. System PATH — kotlin-lsp or groovy-language-server
  4. Install via Mason — If not found, asks whether to install it

Java is resolved similarly: lsp.jre_path config → $JAVA_HOME → system java. (kotlin-lsp is exempt — its native launcher ships a bundled JBR.)

Disabling LSPs

Disable all LSPs:

require("droid").setup({
    lsp = { enabled = false },
})

Disable specific LSP:

require("droid").setup({
    lsp = {
        kotlin = { enabled = true },
        groovy = { enabled = false }, -- Disable Groovy LSP
    },
})

Per-buffer (e.g., in an autocmd or ftplugin):

vim.b.droid_lsp_disabled = true

Per-project config

Create a .droid-lsp.lua in your project root to override Kotlin LSP settings per-project:

-- .droid-lsp.lua
return {
    jre_path = "/usr/lib/jvm/java-21",
    jdk_for_symbol_resolution = "/usr/lib/jvm/java-21",
}

The file is Lua, so droid asks before running it, through the same trust prompt Neovim uses for 'exrc'. Answer "allow" to trust it. After you edit it, Neovim asks again.

Kotlin project sync

lsp = {
  folding = true,          -- fold Kotlin and Groovy buffers by the
                           -- server's foldingRange instead of indentation.
                           -- Folds start open
  kotlin = {
    auto_reload = true,     -- reload the LSP workspace when a build file is saved
  },
}

Auto-reload and :DroidLspRefresh need a server that answers intellij/reloadWorkspace, verified on kotlin-lsp 263.4702.0. On an older build droid warns once, then falls back to :DroidLspRestart.

  • :DroidLspRefresh — manually re-import the project model (Gradle/Maven sync).
  • :DroidLspLog — open the project-sync log; it also holds the full text of any message the server shows.

Import progress is reported as LSP progress, so fidget.nvim, lualine's lsp_status component or vim.lsp.status() render it. Without one of those the import is silent, and :DroidLspLog has the detail. Set lsp.kotlin.import_progress = "off" to stop reporting it at all.

This is the language-server project sync (like Android Studio's "Sync Project with Gradle Files"), not the :DroidBuild APK build.

File renames

:DroidRenameFile asks the server what a rename changes, applies those edits, moves the file, then saves the files it changed. A file that already had unsaved edits is left unsaved for you to review. Renaming PrayerRepository.kt to PrayerStore.kt renames the class and fixes the imports in every file that names it.

Renaming inside a file manager works too when that plugin speaks the LSP file operations. oil.nvim does through its lsp_file_methods, on by default. Its autosave_changes default leaves the edited files unsaved, so consider:

require("oil").setup {
  lsp_file_methods = { autosave_changes = "unmodified" },
}

For nvim-tree or neo-tree, nvim-lsp-file-operations fills the same role.

Decompilation

Navigating to a class from a dependency (e.g., go-to-definition on a library symbol) automatically decompiles the .class file via jar:// and jrt:// protocol handlers.

Debugging (optional, requires nvim-dap)

droid stays dependency-free and does not bundle nvim-dap. If you already use it, wire the Kotlin debug adapter in two lines:

local dap = require("dap")
dap.adapters.intellij_debugger = require("droid.lsp.dap").adapter()
dap.configurations.kotlin = require("droid.lsp.dap").default_configs()

The adapter starts the kotlin-lsp debug server and resolves classpath, the Java executable, and the main-class location automatically. Requires kotlin_ls to be attached.

Editor experience (Kotlin)

Small quality-of-life features for editing .kt buffers. Everything below is on by default and configured under the editor group:

editor = {
  indent = true,          -- restore Neovim's built-in Kotlin indenter
  file_templates = true,  -- offer scaffolds when creating an empty .kt file
  templates = {},         -- extra file templates (merged with the built-ins)
}

Indentation

droid does not ship its own indenter. Neovim already bundles indent/kotlin.vim (GetKotlinIndent), and droid's after/ftplugin/kotlin.lua simply restores GetKotlinIndent() as indentexpr when another plugin (typically nvim-treesitter's unmaintained indent module) has overridden it. Set editor.indent = false to opt out.

This only restores the indent engine — the number of spaces per level comes from your shiftwidth, so droid does not force Kotlin's conventional 4. If Kotlin indents at the wrong width (e.g. 2 while typing), your shiftwidth is 2. Pin it per-project with an .editorconfig (read natively by Neovim, and by IntelliJ/Android Studio and ktlint):

[*.{kt,kts}]
indent_style = space
indent_size = 4

or set it per-filetype in your own after/ftplugin/kotlin.lua (vim.bo.shiftwidth = 4, vim.bo.softtabstop = 4, vim.bo.expandtab = true).

:DroidKdoc

Generates a /** … */ KDoc stub — with @param and @return tags — for the Kotlin function under the cursor. Uses treesitter when available and falls back to a regex parser otherwise.

File templates

Creating a new, empty .kt file offers a picker of scaffolds — Class, Interface, Data class, Object, Enum, and Sealed class — with the package and type name interpolated by the language server. Add your own via editor.templates (merged with the built-ins), or disable the feature with editor.file_templates = false.

Commands

Workflow

Command Description
:DroidRun Pick a variant and a running device, then build, install, launch, and show logcat. Start an emulator first with :DroidEmulator
:DroidBuild Pick a build variant, then build its APK

Gradle

Command Description
:DroidClean Clean project
:DroidSync Sync dependencies
:DroidTask <task> Run any Gradle task
:DroidGradleStop Stop running Gradle task
:DroidGradleModule Open the current module's build.gradle(.kts)
:DroidGradleProject Open the root build.gradle(.kts)
:DroidGradleSettings Open settings.gradle(.kts)
:DroidGradleVersion Open gradle/libs.versions.toml

The jump commands accept modifiers to open a split, for example :vert DroidGradleModule.

Gradle tasks run in the droid panel. Closing the panel leaves the task running, and the panel opens again if the task fails.

Device

Command Description
:DroidEmulator Start an emulator, or pick "+ Create New Emulator" or "- Delete Emulator"
:DroidEmulatorStop Stop emulator
:DroidMirror Mirror device screen (scrcpy)

Deleting an emulator asks which one, then asks to confirm, since its data is lost. Creating an emulator runs in the droid panel, so you can watch its output, and a notification says whether the AVD was created. Starting one runs in the background and notifies once it has booted or failed.

ADB Actions

Command Description
:DroidClearData Clear app data
:DroidForceStop Force stop app
:DroidUninstall Uninstall app

Android CLI (optional)

These commands require the android CLI on PATH. Run :checkhealth droid to verify detection.

Command Description
:DroidScreenshot [path] Capture device screen; opens the PNG with the OS default viewer
:DroidScreenshot! [path] Capture with --annotate (labels UI elements #1, #2, …)
:DroidLayout Show the UI tree of the screen a device is showing, as JSON: text, resource IDs, content descriptions and bounds. Run it again to refresh
:DroidLayout! The same, including non-interactive and hidden elements
:DroidDocs <query> Search the Android Knowledge Base; pick a result to open it in a read-only markdown buffer
:DroidCreate Create a new Android project from a template: asks for the app name, application ID and folder, then offers to switch Neovim to it
:DroidSdk List installed SDK packages, with available updates, in the droid panel
:DroidSdk install [package] Install an SDK package, e.g. platforms/android-34. Without a package, pick from everything available
:DroidSdk update [package] Update one package, or all of them
:DroidSdk remove [package] Remove an SDK package. Without a package, pick one and confirm

When android_cli is active (default "auto" + android on PATH), the emulator commands (:DroidEmulator, :DroidEmulatorStop) route through android emulator …, and :DroidRun uses android run --apks=… (install + launch fused into a single call) instead of gradle install<Variant> + am start.

Note: android emulator is not supported on Windows; the emulator commands fall back to avdmanager/emulator there even when android_cli is active.

Android Studio (optional)

These commands use a running Android Studio through the android CLI. They need android-cli on PATH and Android Studio Quail 2 or newer running with the project open and synced. Each one starts the CLI, so expect a couple of seconds.

Command Description
:DroidLint Show Studio's errors, warnings and Android Lint for the current file as diagnostics
:DroidDeclaration [symbol] Jump to a symbol's declaration using Studio's index (default: the word under the cursor)
:DroidUsages [symbol] List a symbol's usages in the quickfix list using Studio's index
:DroidVersions [ids...] Latest stable and preview versions, e.g. :DroidVersions androidx.compose.ui:ui agp kotlin. With no arguments, looks up the group:artifact on the current line
:DroidStudioOpen Open the current file in Studio

:DroidDeclaration and :DroidUsages reach across modules, which helps where the Kotlin language server cannot (see Known Limitations). Studio reads files from disk, so save before :DroidLint.

With XDG_CONFIG_HOME set, Studio registers itself in $XDG_CONFIG_HOME/.android while the CLI looks in ~/.android and finds no Studio. droid detects this and points the CLI at Studio's folder for these commands. The CLI installs a copy of itself there the first time (about 160 MB). :checkhealth droid shows whether Studio is reachable.

Logcat

Command Description
:DroidLogcat Open logcat
:DroidLogcatFilter log_level=d Filter by level
:DroidLogcatFilter tag=MyTag Filter by tag
:DroidLogcatFilter package=mine Filter by package
:DroidLogcatFilter grep=Exception Keep lines containing the text
:DroidLogcatClear Clear the logcat buffer (keeps streaming)
:DroidLogcatStop Stop logcat

Combine filters: :DroidLogcatFilter tag=MyTag log_level=d

The panel scrolls with new lines while the cursor is on the last line. Move up to read back, and it stops following until you return to the bottom. When filtering by package, logcat follows the app to its new process after a crash or relaunch, keeping the lines already shown.

LSP Commands

These commands work in .kt and .groovy buffers with their respective LSP attached.

Command Description
:DroidImports Organize imports (Kotlin)
:DroidFormat Format buffer, or the selected lines in visual mode
:DroidSymbols Document symbols (opens location list - navigate with :lnext, :lprev, :lfirst, :llast)
:DroidWorkspaceSymbols Workspace symbol search (opens location list - navigate with :lnext, :lprev)
:DroidReferences Find all references (opens quickfix list - navigate with :cnext, :cprev, :cfirst, :clast)
:DroidCallHierarchy [incoming|outgoing] Calls to the symbol under the cursor, or calls it makes (default incoming)
:DroidTypeHierarchy [subtypes|supertypes] Implementations of the type under the cursor, or its base types (default subtypes)
:DroidRename Rename symbol
:DroidRenameFile [path] Rename the current file, updating its class name and every import of it (prompts when no path is given)
:DroidCodeAction Show code actions
:DroidQuickFix Quick fix for diagnostics on current line
:DroidInlayHintsToggle Toggle inlay hints for current buffer
:DroidHintsToggle Toggle HINT-severity diagnostics
:DroidLspRefresh Reload the Kotlin LSP workspace (re-import project model)
:DroidLspLog Open the Kotlin LSP project-sync log
:DroidKdoc Generate a KDoc stub for the Kotlin function under the cursor
:DroidExportWorkspace Export workspace config to JSON (Kotlin only)
:DroidCleanWorkspace Stop all LSPs and clean cached workspaces
:DroidLspStop Stop all LSP servers
:DroidLspRestart Restart all LSP servers

Keybindings

-- Workflow
vim.keymap.set("n", "<leader>ar", ":DroidRun<CR>")
vim.keymap.set("n", "<leader>ab", ":DroidBuild<CR>")

-- Gradle
vim.keymap.set("n", "<leader>as", ":DroidSync<CR>")
vim.keymap.set("n", "<leader>ac", ":DroidClean<CR>")

-- Device
vim.keymap.set("n", "<leader>ae", ":DroidEmulator<CR>")
vim.keymap.set("n", "<leader>am", ":DroidMirror<CR>")

-- Logcat
vim.keymap.set("n", "<leader>al", ":DroidLogcat<CR>")
vim.keymap.set("n", "<leader>ax", ":DroidLogcatStop<CR>")

-- LSP
vim.keymap.set("n", "<leader>ao", ":DroidImports<CR>")
vim.keymap.set("n", "<leader>af", ":DroidFormat<CR>")
vim.keymap.set("n", "gs", ":DroidWorkspaceSymbols<CR>")
vim.keymap.set("n", "gr", ":DroidReferences<CR>")

-- Android Studio (needs Studio running with the project open)
vim.keymap.set("n", "<leader>aL", ":DroidLint<CR>")
vim.keymap.set("n", "<leader>ad", ":DroidDeclaration<CR>")
vim.keymap.set("n", "<leader>au", ":DroidUsages<CR>")

Known Limitations

Kotlin LSP Cross-File Navigation

JetBrains' Kotlin LSP is experimental and currently has limited support for Android Gradle projects. While hover, completion, and diagnostics work within the current file, go-to-definition and find-references across files may not work reliably for Android projects.

The LSP successfully detects the Gradle project structure but does not build a complete workspace-wide symbol index for Android modules. This is a known limitation of the upstream Kotlin LSP implementation, not droid.nvim.

What works:

  • Hover and type information for symbols in the current file
  • Code completion within the current file
  • Diagnostics and error checking
  • Inlay hints
  • Document symbols (:DroidSymbols)
  • Organize imports (:DroidImports)

What may not work:

  • Jetpack Compose or KMP is not supported yet. kotlin-lsp reports every @Composable function as badly named, so droid hides FunctionName on annotated declarations by default (suppress_when_annotated)
  • Go-to-definition across files (e.g., jumping from MainActivity to MainViewModel in another file)
  • Find-references across the workspace
  • Workspace symbol search (:DroidWorkspaceSymbols)

Workarounds:

  • Use :DroidBuild to catch compilation errors
  • Use Telescope or grep for finding symbol definitions: :Telescope live_grep or :Telescope grep_string
  • Use :DroidReferences for same-file references (opens quickfix list)
  • With Android Studio running and the project open, use :DroidDeclaration and :DroidUsages. They ask Studio's index, which reaches across files and modules

Note: This limitation is specific to Kotlin LSP with Android Gradle projects. Pure JVM Kotlin projects may have better support. The JetBrains Kotlin LSP README states: "currently, only JVM-only Kotlin Gradle projects are supported out-of-the box."

License

MIT

Releases

Sponsor this project

Packages

Contributors

Languages