24 KiB
Meshtastic Android - Agent Guide
This file serves as a comprehensive guide for AI agents and developers working on the Meshtastic-Android codebase. Use this as your primary reference for understanding the architecture, conventions, and strict rules of this project.
For execution-focused recipes, see docs/agent-playbooks/README.md.
1. Project Vision & Architecture
Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, decentralized mesh networks. The goal is to decouple business logic from the Android framework, enabling future expansion to iOS and other platforms while maintaining a high-performance native Android experience.
- Language: Kotlin (primary), AIDL.
- Build System: Gradle (Kotlin DSL). JDK 21 is REQUIRED.
- Target SDK: API 36. Min SDK: API 26 (Android 8.0).
- Flavors:
fdroid: Open source only, no tracking/analytics.google: Includes Google Play Services (Maps) and DataDog analytics (RUM, Session Replay, Compose action tracking, customconnectRUM action). 100% sampling, Apple-parity environments ("Local"/"Production").
- Core Architecture: Modern Android Development (MAD) with KMP core.
- KMP Modules: Most
core:*modules. All declarejvm(),iosArm64(), andiosSimulatorArm64()targets and compile clean across all. - Android-only Modules:
core:api(AIDL),core:barcode(CameraX + flavor-specific decoder). Shared contracts abstracted intocore:ui/commonMain. - UI: Jetpack Compose Multiplatform (Material 3).
- DI: Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in
appanddesktop. - Navigation: JetBrains Navigation 3 (Scene-based architecture) with shared backstack state. Deep linking uses RESTful paths (e.g.
/nodes/1234) parsed byDeepLinkRouterincore:navigation. - Lifecycle: JetBrains multiplatform
lifecycle-viewmodel-composeandlifecycle-runtime-compose. - Adaptive UI: Material 3 Adaptive (v1.3+) with support for Large (1200dp) and Extra-large (1600dp) breakpoints.
- Database: Room KMP.
- KMP Modules: Most
2. Codebase Map
| Directory | Description |
|---|---|
app/ |
Main application module. Contains MainActivity, Koin DI modules, and app-level logic. Uses package org.meshtastic.app. |
build-logic/ |
Convention plugins for shared build configuration (e.g., meshtastic.kmp.feature, meshtastic.kmp.library, meshtastic.kmp.jvm.android, meshtastic.koin). |
config/ |
Detekt static analysis rules (config/detekt/detekt.yml) and Spotless formatting config (config/spotless/.editorconfig). |
docs/ |
Architecture docs and agent playbooks. See docs/agent-playbooks/README.md for version baseline and task recipes. |
core/model |
Domain models and common data structures. |
core:proto |
Protobuf definitions (Git submodule). |
core:common |
Low-level utilities, I/O abstractions (Okio), and common types. |
core:database |
Room KMP database implementation. |
core:datastore |
Multiplatform DataStore for preferences. |
core:repository |
High-level domain interfaces (e.g., NodeRepository, LocationRepository). |
core:domain |
Pure KMP business logic and UseCases. |
core:data |
Core manager implementations and data orchestration. |
core:network |
KMP networking layer using Ktor, MQTT abstractions, and shared transport (StreamFrameCodec, TcpTransport, SerialTransport, BleRadioInterface). |
core:di |
Common DI qualifiers and dispatchers. |
core:navigation |
Shared navigation keys/routes for Navigation 3, DeepLinkRouter for typed backstack synthesis, and MeshtasticNavSavedStateConfig for backstack persistence. |
core:ui |
Shared Compose UI components (MeshtasticAppShell, MeshtasticNavDisplay, MeshtasticNavigationSuite, AlertHost, SharedDialogs, PlaceholderScreen, MainAppBar, dialogs, preferences) and platform abstractions. |
core:service |
KMP service layer; Android bindings stay in androidMain. |
core:api |
Public AIDL/API integration module for external clients. |
core:prefs |
KMP preferences layer built on DataStore abstractions. |
core:barcode |
Barcode scanning (Android-only). |
core:nfc |
NFC abstractions (KMP). Android NFC hardware implementation in androidMain. |
core/ble/ |
Bluetooth Low Energy stack using Kable. |
core/resources/ |
Centralized string and image resources (Compose Multiplatform). |
core/testing/ |
Shared test doubles, fakes, and utilities for commonTest across all KMP modules. |
feature/ |
Feature modules (e.g., settings, map, messaging, node, intro, connections, firmware, wifi-provision, widget). All are KMP with jvm() and ios() targets except widget. Use meshtastic.kmp.feature convention plugin. |
feature/wifi-provision |
KMP WiFi provisioning via BLE (Nymea protocol). Scans for provisioning devices, lists available networks, applies credentials. Uses core:ble Kable abstractions. |
feature/firmware |
Fully KMP firmware update system: Unified OTA (BLE + WiFi via Kable/Ktor), native Nordic Secure DFU protocol (pure KMP, no Nordic library), USB/UF2 updates, and FirmwareRetriever with manifest-based resolution. Desktop is a first-class target. |
desktop/ |
Compose Desktop application — first non-Android KMP target. Thin host shell relying entirely on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports with want_config handshake. |
mesh_service_example/ |
DEPRECATED — scheduled for removal. Legacy sample app showing core:api service integration. Do not add code here. See core/api/README.md for the current integration guide. |
3. Development Guidelines & Coding Standards
A. UI Development (Jetpack Compose)
- Material 3: The app uses Material 3.
- Strings: MUST use the Compose Multiplatform Resource library in
core:resources(stringResource(Res.string.your_key)). For ViewModels or non-composable Coroutines, use the asynchronousgetStringSuspend(Res.string.your_key). NEVER use hardcoded strings, and NEVER use the blockinggetString()in a coroutine. - String formatting: CMP's
stringResource(res, args)/getString(res, args)only support%N$s(string) and%N$d(integer) positional specifiers. Float formats like%N$.1fare NOT supported — they pass through unsubstituted. For float values, pre-format in Kotlin usingNumberFormatter.format(value, decimalPlaces)fromcore:commonand pass the result as a%N$sstring arg. Use bare%(not%%) for literal percent signs in CMP-consumed strings, since CMP does not convert%%to%. For JVM-only code usingformatString()(which wrapsString.format()), full printf specifiers including%N$.Nfand%%are supported. - Dialogs: Use centralized components in
core:ui(e.g.,MeshtasticResourceDialog). - Alerts: Use
AlertHost(alertManager)fromcore:ui/commonMainin each platform host shell (Main.kt,DesktopMainScreen.kt). For global responses like traceroute and firmware validation, use the specialized common handlers:TracerouteAlertHandler(uiViewModel)andFirmwareVersionCheck(uiViewModel). Do NOT duplicate inline alert-rendering logic or trigger alerts directly during composition. For shared QR/contact dialogs, use theSharedDialogs(uiViewModel)composable. - Placeholders: For desktop/JVM features not yet implemented, use
PlaceholderScreen(name)fromcore:ui/commonMain. Do NOT define inline placeholder composables in feature modules. - Theme Picker: Use
ThemePickerDialogandThemeOptionfromfeature:settings/commonMain. Do NOT duplicate the theme dialog or enum in platform-specific source sets. - Adaptive Layouts: Use
currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)to support the 2026 Desktop Experience breakpoints. Prioritize higher information density and mouse-precision interactions for Desktop and External Display (Android 16 QPR3) targets. Investigate 3-pane "Power User" scenes (e.g., Node List + Detail + Map/Charts) using Navigation 3 Scenes,extraPane(), and draggable dividers (VerticalDragHandle+paneExpansionState) for widths ≥ 1200dp. - Platform/Flavor UI: Inject platform-specific behavior (e.g., map providers) via
CompositionLocalfromapp.
B. Logic & Data Layer
- KMP Focus: All business logic must reside in
commonMainof the respectivecoremodule. - Platform purity: Never import
java.*orandroid.*incommonMain. Use KMP alternatives:java.util.Locale→ Kotlinuppercase()/lowercase()orexpect/actual.java.util.concurrent.ConcurrentHashMap→atomicfuorMutex-guardedmutableMapOf().java.util.concurrent.locks.*→kotlinx.coroutines.sync.Mutex.java.io.*→ Okio (BufferedSource/BufferedSink). Note: JetBrains now recommendskotlinx-ioas the official Kotlin I/O library (built on Okio). This project standardizes on Okio directly; do not migrate without explicit decision.kotlinx.coroutines.Dispatchers.IO→org.meshtastic.core.common.util.ioDispatcher(expect/actual). Note:Dispatchers.IOis available incommonMainsince kotlinx.coroutines 1.8.0, but this project uses theioDispatcherwrapper for consistency.
- Shared helpers over duplicated lambdas: When
androidMainandjvmMaincontain identical pure-Kotlin logic (formatting, action dispatch, validation), extract it to a function incommonMain. Examples:formatLogsTo()infeature:settings,handleNodeAction()infeature:node,findNodeByNameSuffix()infeature:connections,MeshtasticAppShellincore:ui/commonMain, andBaseRadioTransportFactoryincore:network/commonMain. - KMP file naming: In KMP modules,
commonMainand platform source sets (androidMain,jvmMain) share the same package namespace. If both contain a file with the same name (e.g.,LogExporter.kt), the Kotlin/JVM compiler will produce a duplicate class error. Use distinct filenames: keep theexpectdeclaration inLogExporter.ktand put shared helpers in a separate file likeLogFormatter.kt. jvmAndroidMainsource set: Modules that share JVM-specific code between Android and Desktop apply themeshtastic.kmp.jvm.androidconvention plugin. This creates ajvmAndroidMainsource set via Kotlin's hierarchy template API. Used incore:common,core:model,core:data,core:network, andcore:ui.- Hierarchy template first: Prefer Kotlin's default hierarchy template and convention plugins over manual
dependsOn(...)graphs. Manual source-set wiring should be reserved for cases the template cannot model. expect/actualrestraint: Prefer interfaces + DI for platform capabilities; useexpect/actualfor small unavoidable platform primitives. Avoid broad expect/actual class hierarchies when an interface-based boundary is sufficient.- Feature navigation graphs: Feature modules export Navigation 3 graph functions as extension functions on
EntryProviderScope<NavKey>incommonMain(e.g.,fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>)). Host shells (app,desktop) assemble these into a singleentryProvider<NavKey>block. Do NOT define navigation graphs in platform-specific source sets. - Concurrency: Use Kotlin Coroutines and Flow.
- Dependency Injection: Use Koin Annotations with the K2 compiler plugin (
koin-pluginin version catalog). Thekoin-annotationslibrary version is unified withkoin-core(both useversion.ref = "koin"). TheKoinConventionPluginuses the typedKoinGradleExtensionto configure the K2 plugin (e.g.,compileSafety.set(false)). Keep root graph assembly inapp. - ViewModels: Follow the MVI/UDF pattern. Use the multiplatform
androidx.lifecycle.ViewModelincommonMain. BothappanddesktopuseMeshtasticNavDisplayfromcore:ui/commonMain, which configuresViewModelStoreNavEntryDecorator+SaveableStateHolderNavEntryDecorator. ViewModels obtained viakoinViewModel()insideentry<T>blocks are scoped to the entry's backstack lifetime and cleared on pop. - Navigation host: Use
MeshtasticNavDisplayfromcore:ui/commonMaininstead of callingNavDisplaydirectly. It provides entry-scoped ViewModel decoration,DialogSceneStrategyfor dialog entries, and a shared 350 ms crossfade transition. Host modules (app,desktop) should NOT configureentryDecorators,sceneStrategies, ortransitionSpecthemselves. - BLE: All Bluetooth communication must route through
core:bleusing Kable. - Networking: Pure Ktor — no OkHttp anywhere. Engines:
ktor-client-androidfor Android,ktor-client-javafor desktop/JVM. Use KtorLoggingplugin for HTTP debug logging (not OkHttp interceptors).HttpClientis provided via Koin inapp/di/NetworkModuleandcore:network/di/CoreNetworkAndroidModule. - Image Loading (Coil): Use
coil-network-ktor3withKtorNetworkFetcherFactoryon all platforms.ImageLoaderis configured in host modules only (appvia Koin@Single,desktopviasetSingletonImageLoaderFactory). Feature modules depend only onlibs.coil(coil-compose) forAsyncImage— never addcoil-network-*orcoil-svgto feature modules. - Dependencies: Check
gradle/libs.versions.tomlbefore assuming a library is available. - JetBrains fork aliases: Version catalog aliases for JetBrains-forked AndroidX artifacts use the
jetbrains-*prefix (e.g.,jetbrains-lifecycle-runtime-compose,jetbrains-navigation3-ui). Plainandroidx-*aliases are true Google AndroidX artifacts. Never mix them up incommonMain. - Compose Multiplatform: Version catalog aliases for Compose Multiplatform artifacts use the
compose-multiplatform-*prefix (e.g.,compose-multiplatform-material3,compose-multiplatform-foundation). Never use plainandroidx.composedependencies in common Main. - Room KMP: Always use
factory = { MeshtasticDatabaseConstructor.initialize() }inRoom.databaseBuilderandinMemoryDatabaseBuilder. DAOs and Entities reside incommonMain. - QR Codes: Use
rememberQrCodePainterfromcore:ui/commonMain(powered byqrcode-kotlin) for generating QR codes. Do not use Android Bitmap or ZXing APIs in common code. - Testing: Write ViewModel and business logic tests in
commonTest. UseTurbinefor Flow testing,Kotestfor property-based testing, andMokkeryfor mocking. Usecore:testingshared fakes. - Build-logic conventions: In
build-logic/convention, prefer lazy Gradle configuration (configureEach,withPlugin, provider APIs). AvoidafterEvaluatein convention plugins unless there is no viable lazy alternative.
C. Namespacing
- Standard: Use the
org.meshtastic.*namespace for all code. - Legacy: Maintain the
com.geeksville.meshApplication ID.
4. Execution Protocol
A. Environment Setup
- JDK 21 MUST be used to prevent Gradle sync/build failures.
- Secrets: You must copy
secrets.defaults.propertiestolocal.properties:MAPS_API_KEY=dummy_key datadogApplicationId=dummy_id datadogClientToken=dummy_token
B. Strict Execution Commands
Always run commands in the following order to ensure reliability. Do not attempt to bypass clean if you are facing build issues.
Baseline (recommended order):
./gradlew clean
./gradlew spotlessCheck
./gradlew spotlessApply
./gradlew detekt
./gradlew assembleDebug
./gradlew test allTests
Testing:
# Full host-side unit test run (required — see note below):
./gradlew test allTests
# Pure-Android / pure-JVM modules only (app, desktop, core:api, core:barcode, feature:widget, mesh_service_example):
./gradlew test
# KMP modules only (all core:* KMP + all feature:* KMP modules — jvmTest + testAndroidHostTest + iosSimulatorArm64Test):
./gradlew allTests
# CI-aligned flavor-explicit Android unit tests:
./gradlew testFdroidDebugUnitTest testGoogleDebugUnitTest
./gradlew connectedAndroidTest # Run instrumented tests
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks
Why
test allTestsand not justtest: In KMP modules, thetesttask name is ambiguous — Gradle matches bothtestAndroidandtestAndroidHostTestand refuses to run either, silently skipping all 25 KMP modules.allTestsis theKotlinTestReportlifecycle task registered by the KMP Gradle plugin for each KMP module. It runsjvmTest,testAndroidHostTest(where declared withwithHostTest {}), andiosSimulatorArm64Test(disabled at execution — iOS targets are compile-only). Conversely,allTestsdoes not cover the pure-Android modules (:app,:core:api,:core:barcode,:feature:widget,:mesh_service_example,:desktop), which is why both are needed.
Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin your tests to @Config(sdk = [34]) to avoid SDK 35 compatibility crashes.
CI workflow conventions (GitHub Actions):
- Reusable CI in
.github/workflows/reusable-check.ymlis structured as four parallel job groups:lint-check— Runs spotless, detekt, Android lint, and KMP smoke compile in a single Gradle invocation. Usesfetch-depth: 0(full clone) for spotless ratcheting and version code calculation. Producescache_read_onlyoutput and computedversion_codefor downstream jobs.test-shards— A 3-shard matrix that runs unit tests in parallel (depends onlint-check):shard-core:allTestsfor allcore:*KMP modules.shard-feature:allTestsfor allfeature:*KMP modules.shard-app: Explicit test tasks for pure-Android/JVM modules (app,desktop,core:barcode,mesh_service_example). Each shard generates its own Kover XML coverage and uploads test results + coverage to Codecov with per-shard flags. Downstream jobs (test-shards, android-check, build-desktop) usefetch-depth: 1and receiveVERSION_CODEfrom lint-check via env var, enabling shallow clones.
android-check— Builds APKs and runs instrumented tests (depends onlint-check).build-desktop— Desktop packaging (depends onlint-check).
- Test sharding uses
fail-fast: falseso a failure in one shard does not cancel the others. - JUnit Platform parallel execution is enabled project-wide with classes running sequentially (
junit.jupiter.execution.parallel.mode.classes.default=same_thread) to avoidDispatchers.setMain()races (JVM-global singleton used by 19+ ViewModel test classes). Cross-module parallelism comes from Gradle forks (maxParallelForks). test-retryplugin (maxRetries=2, maxFailures=10) is applied to all module types:AndroidApplicationConventionPlugin,AndroidLibraryConventionPlugin, andKmpLibraryConventionPlugin.- Android matrix job runs explicit assemble tasks for
appandmesh_service_example; instrumentation is enabled by input and matrix API. - Prefer explicit Gradle task paths in CI (for example
app:lintFdroidDebug,app:connectedGoogleDebugAndroidTest) instead of shorthand tasks likelintDebug. - Pull request CI is main-only (
.github/workflows/pull-request.ymltargetsmainbranch). - Gradle cache writes are trusted on
mainand merge queue runs (merge_group/gh-readonly-queue/*); other refs use read-only cache mode in reusable CI. - PR
check-changespath filtering lives in.github/workflows/pull-request.ymland must include module dirs plus build/workflow entrypoints (build-logic/**,gradle/**,.github/workflows/**,gradlew,settings.gradle.kts, etc.) so CI is not skipped for infra-only changes. - Runner strategy (three tiers):
ubuntu-24.04-arm— Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). These run only shell scripts or GitHub API calls and benefit from ARM runners' shorter queue times.ubuntu-24.04— Main Gradle-heavy jobs (CIlint-check/test-shards/android-check, release builds, Dokka, CodeQL, publish, dependency-submission). Pin where possible for reproducibility.- Desktop runners: Reusable CI uses
ubuntu-24.04for thebuild-desktopjob in.github/workflows/reusable-check.yml; release packaging matrix remains[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm].
- CI Gradle properties:
gradle.propertiesis tuned for local dev (8g heap, 4g Kotlin daemon). CI uses.github/ci-gradle.properties, which thegradle-setupcomposite action copies to~/.gradle/gradle.propertiesbefore any Gradle invocation. Key CI overrides:org.gradle.daemon=false(single-use runners),kotlin.incremental=false(fresh checkouts),-Xmx4gGradle heap,-Xmx2gKotlin daemon, VFS watching disabled, workers capped at 4,org.gradle.isolated-projects=truefor better parallelism. Disables unused Android build features (resvalues,shaders). This follows the nowinandroidci-gradle.propertiespattern. - CI optimization strategies (2026): Applied comprehensive CI optimizations (P0-P3):
- P0 (merged Gradle invocations):
lint-checkmerges spotlessCheck, detekt, android lint, and kmpSmokeCompile into a single Gradle invocation to avoid 3x cold-start overhead. Usesfilter: 'blob:none'for blobless git clone. Switches submodules from'recursive'to boolean (saves overhead on nested submodule discovery). - P1 (reduced PR overhead): Added
run_coverageworkflow input (default: true); PRs skip Kover reports via conditional tasks in test-shards matrix. IncreasedmaxParallelForksin CI to use all available processors (4 on standard runners) whenci=trueproperty is set, vs. half locally for system responsiveness. - P2 (build feature optimization): Detekt disables non-essential report formats in CI (html, txt, md); retains only xml + sarif for GitHub annotations. Disables unused Android build features (resvalues, shaders) in
ci-gradle.properties. - P3 (structural improvement): Removed
verify-check-changes-filterfromvalidate-and-builddependencies; it now runs in parallel as a standalone required check instead of gating the main build.
- P0 (merged Gradle invocations):
maxParallelForksCI logic: ProjectExtensions.kt line ~79 checksproject.findProperty("ci") == "true"and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass-Pci=trueto enable this.- Detekt report formats: Detekt.kt line ~44 checks
project.findProperty("ci") == "true"and disables html, txt, md reports in CI; only xml + sarif are required for GitHub reporting. - KMP Smoke Compile: Use
./gradlew kmpSmokeCompileinstead of listing individual module compile tasks. ThekmpSmokeCompilelifecycle task (registered inRootConventionPlugin) auto-discovers all KMP modules and depends on theircompileKotlinJvm+compileKotlinIosSimulatorArm64tasks. mavenLocal()gated: ThemavenLocal()repository is disabled by default to prevent CI cache poisoning. For local JitPack testing, pass-PuseMavenLocalto Gradle.- Terminal Pagers: When running shell commands like
git difforgit log, ALWAYS use--no-pager(e.g.,git --no-pager diff) to prevent the agent from getting stuck in an interactive prompt. - Text Search: Prefer using
rg(ripgrep) overgreporfindfor fast text searching across the codebase.
C. Documentation Sync
AGENTS.md is the single source of truth for agent instructions. .github/copilot-instructions.md and GEMINI.md are thin stubs that redirect here — do NOT duplicate content into them.
When you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update AGENTS.md, docs/agent-playbooks/*, docs/kmp-status.md, and docs/decisions/architecture-review-2026-03.md as needed.
5. Troubleshooting
- Build Failures: Check
gradle/libs.versions.tomlfor dependency conflicts. - Missing Secrets: Check
local.properties. - JDK Version: JDK 21 is required.
- Configuration Cache: Add
--no-configuration-cacheflag if cache-related issues persist. - Koin Injection Failures: Verify the KMP component is included in
approot module wiring (AppKoinModule).