Files
Meshtastic-Android/docs/en/developer/architecture.md
James Rich 94bcec85b3 refactor: Naming cleanup for clarity and Kotlin conventions
Audit-driven renames:
- DirectRadioControllerImpl -> RadioControllerImpl: the "Direct" prefix
  was vestigial (it once contrasted with the AIDL-routed
  AndroidRadioControllerImpl, now deleted); it is the sole impl and now
  matches its parts (AdminControllerImpl, etc.).
- RadioController.getPacketId() -> generatePacketId(): a `get`-prefixed
  function that generates a fresh id each call violates the
  getter-is-idempotent convention; also aligns with
  CommandSender.generatePacketId().
- RequestController -> QueryController (+ Impl, + the `requestController`
  params/mocks): clearer intent for the pull/query surface; "request" was
  generic.
- RequestTimer param `label` -> `logLabel`.
- AdminControllerImpl DEFAULT_REBOOT_DELAY -> DEFAULT_DELAY_SECONDS
  (shared by reboot + shutdown; conveys the unit).

Interface-consumer-safe; docs/READMEs/architecture guide updated to match.
Koin graph verifies on Android + desktop; affected test suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:37:54 -05:00

7.0 KiB

title, parent, nav_order, last_updated, aliases
title parent nav_order last_updated aliases
Architecture Developer Guide 1 2026-05-29
layers
module-architecture
kmp
radio-control

Architecture

The Meshtastic Android/Desktop/iOS application follows a modular Kotlin Multiplatform (KMP) architecture with clear layer boundaries.

Layer Overview

┌─────────────────────────────────────────────┐
│          androidApp / desktopApp            │  Platform entry points
├─────────────────────────────────────────────┤
│              feature/* modules               │  UI + Business Logic
├─────────────────────────────────────────────┤
│                core/* modules                │  Shared infrastructure
├─────────────────────────────────────────────┤
│           Platform (Android/JVM/iOS)         │  OS-specific bindings
└─────────────────────────────────────────────┘

Module Categories

androidApp/ — Android Application

The Android application entry point:

  • Activity, Application, and Manifest definitions
  • Koin DI module composition (AppKoinModule)
  • Flavor-specific bindings (google/, fdroid/)
  • Android-only integrations (widgets, services)

desktopApp/ — Desktop JVM Application

The Desktop (Linux/macOS/Windows) entry point:

  • Compose Desktop window management
  • Desktop-specific DI (DesktopKoinModule)
  • Platform stubs for Android-only capabilities
  • BLE (Kable), Serial, and TCP transport implementations

feature/* — Feature Modules

Each feature/ module owns a vertical slice of functionality:

Module Responsibility
feature:intro Onboarding/welcome flow
feature:messaging Messages, channels, contacts, quick chat
feature:connections Bluetooth/USB/TCP connection management
feature:map Map display, waypoints
feature:node Node list, node detail, metrics
feature:settings All configuration screens
feature:firmware Firmware update flow
feature:docs In-app documentation browser
feature:wifi-provision WiFi provisioning
feature:widget Android home screen widgets

Feature modules:

  • Use the meshtastic.kmp.feature convention plugin
  • Depend on core modules, never on other feature modules
  • Own their navigation entries and DI registrations
  • Contain platform-specific implementations in androidMain/jvmMain/iosMain

core/* — Core Modules

Shared infrastructure used by all features:

Module Responsibility
core:common Utilities, extensions, build config
core:navigation Routes, deep links, Navigation 3
core:ui Shared Compose components, icons, theme
core:resources Shared string resources
core:model Domain models
core:data Data layer abstractions
core:database Room KMP database
core:datastore DataStore preferences
core:prefs App preferences
core:repository Repository interfaces
core:service Mesh service layer
core:di DI utilities
core:network HTTP/serial/transport
core:ble Bluetooth LE abstractions
core:proto Protobuf definitions
core:testing Test utilities

KMP Source Sets

Each module uses the standard KMP source set hierarchy:

src/
├── commonMain/     ← Shared code (all platforms)
├── commonTest/     ← Shared tests
├── androidMain/    ← Android-specific
├── jvmMain/        ← Desktop JVM-specific
├── iosMain/        ← iOS-specific
└── jvmTest/        ← Desktop test host

Golden Rules:

  • No android.* imports in commonMain
  • Platform-specific code goes in appropriate source set
  • Prefer interfaces + DI over expect/actual for complex behaviors
  • Use expect/actual only for simple declarations

Dependency Injection

The project uses Koin with annotation processing:

  • @Module, @Single, @Factory annotations
  • @ComponentScan for automatic registration
  • Feature modules export their own Feature*Module class
  • App/Desktop compose all modules in their root DI configuration

Radio Control

Features issue radio commands through RadioController (core:repository), a composite of four focused sub-interfaces so callers can depend on just the slice they need:

Sub-interface Responsibility
AdminController Config, channels, owner, device lifecycle, editSettings { } transactions
MessagingController Send packets, reactions, shared contacts
NodeController Favorite, ignore, mute, remove nodes
QueryController Telemetry, traceroute, position/user-info queries

RadioControllerImpl (core:service) is the in-process composition root for all targets (Desktop, iOS, single-process Android). It assembles the four sub-controllers via Kotlin interface delegation and adds the cross-cutting concerns (connection state, packet-id, location, device-address switching). Commands are direct suspend calls; admin writes are fire-and-forget because the device is the source of truth (local persistence is an optimistic cache). The layered shape mirrors the meshtastic-sdk AdminApi/TelemetryApi design to ease a future SDK migration.

Service Repository

ServiceRepository is the reactive bridge between the mesh service and all feature/UI layers. It is decomposed into focused provider interfaces following the Interface Segregation Principle:

Interface Responsibility
ConnectionStateProvider Read-only connectionState: StateFlow<ConnectionState>
TracerouteResponseProvider Traceroute response state + clear
NeighborInfoResponseProvider Neighbor info response state + clear
ServiceStateWriter Write-side for handlers (set*, emit*, clear*)

ServiceRepository extends all four interfaces — consumers inject the narrowest interface they actually need. For example, ContactsViewModel injects only ConnectionStateProvider rather than the entire ServiceRepository, preventing accidental access to write operations from UI code. RadioController also extends ConnectionStateProvider so VMs that already inject a controller sub-interface can read connection state without a separate dependency.

Navigation

Navigation uses Navigation 3 with typed routes:

  • All routes defined in core/navigation/Routes.kt
  • Routes are @Serializable data classes/objects
  • Deep links resolved through DeepLinkRouter
  • Each feature registers its own navigation entries

See Navigation & Deep Links for details.