feat: Complete app module thinning and feature module extraction (#4844)

This commit is contained in:
James Rich 2026-03-18 19:21:18 -05:00 committed by GitHub
parent dcbbc0823b
commit 1b0dc75dfe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 439 additions and 119 deletions

View file

@ -9,7 +9,7 @@ The `:app` module is the entry point for the Meshtastic Android application. It
The single Activity of the application. It hosts the `NavHost` and manages the root UI structure (Navigation Bar, Rail, etc.).
### 2. `MeshService`
The core background service that manages long-running communication with the mesh radio. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
The core background service that manages long-running communication with the mesh radio. While it is declared in the `:app` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
### 3. Koin Application
`MeshUtilApplication` is the Koin entry point, providing the global dependency injection container.

View file

@ -234,6 +234,7 @@ dependencies {
implementation(projects.feature.node)
implementation(projects.feature.settings)
implementation(projects.feature.firmware)
implementation(projects.feature.widget)
implementation(libs.jetbrains.compose.material3.adaptive)
implementation(libs.jetbrains.compose.material3.adaptive.layout)

View file

@ -257,7 +257,7 @@
<receiver android:name="org.meshtastic.core.service.ReactionReceiver" android:exported="false" />
<receiver
android:name="org.meshtastic.app.widget.LocalStatsWidgetReceiver"
android:name="org.meshtastic.feature.widget.LocalStatsWidgetReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />

View file

@ -59,6 +59,7 @@ import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.nfc.NfcScannerEffect
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.channel_invalid
import org.meshtastic.core.service.MeshServiceClient
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider

View file

@ -39,11 +39,11 @@ import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin
import org.meshtastic.app.di.AppKoinModule
import org.meshtastic.app.di.module
import org.meshtastic.app.widget.LocalStatsWidgetReceiver
import org.meshtastic.core.common.ContextServices
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.repository.MeshPrefs
import org.meshtastic.core.service.worker.MeshLogCleanupWorker
import org.meshtastic.feature.widget.LocalStatsWidgetReceiver
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
@ -92,7 +92,7 @@ open class MeshUtilApplication :
pushPreview()
val widgetStateProvider: org.meshtastic.app.widget.LocalStatsWidgetStateProvider = get()
val widgetStateProvider: org.meshtastic.feature.widget.LocalStatsWidgetStateProvider = get()
try {
// Wait for real data for up to 30 seconds before pushing an updated preview
withTimeout(30.seconds) {

View file

@ -52,6 +52,7 @@ import org.meshtastic.feature.map.di.FeatureMapModule
import org.meshtastic.feature.messaging.di.FeatureMessagingModule
import org.meshtastic.feature.node.di.FeatureNodeModule
import org.meshtastic.feature.settings.di.FeatureSettingsModule
import org.meshtastic.feature.widget.di.FeatureWidgetModule
@Module(
includes =
@ -82,6 +83,7 @@ import org.meshtastic.feature.settings.di.FeatureSettingsModule
FeatureSettingsModule::class,
FeatureFirmwareModule::class,
FeatureIntroModule::class,
FeatureWidgetModule::class,
NetworkModule::class,
FlavorModule::class,
],

View file

@ -67,13 +67,6 @@ import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.BuildConfig
import org.meshtastic.app.navigation.channelsGraph
import org.meshtastic.app.navigation.connectionsGraph
import org.meshtastic.app.navigation.contactsGraph
import org.meshtastic.app.navigation.firmwareGraph
import org.meshtastic.app.navigation.mapGraph
import org.meshtastic.app.navigation.nodesGraph
import org.meshtastic.app.navigation.settingsGraph
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.DeviceVersion
@ -108,6 +101,13 @@ import org.meshtastic.core.ui.util.annotateTraceroute
import org.meshtastic.core.ui.util.toMessageRes
import org.meshtastic.core.ui.viewmodel.UIViewModel
import org.meshtastic.feature.connections.ScannerViewModel
import org.meshtastic.feature.connections.navigation.connectionsGraph
import org.meshtastic.feature.firmware.navigation.firmwareGraph
import org.meshtastic.feature.map.navigation.mapGraph
import org.meshtastic.feature.messaging.navigation.contactsGraph
import org.meshtastic.feature.node.navigation.nodesGraph
import org.meshtastic.feature.settings.navigation.channelsGraph
import org.meshtastic.feature.settings.navigation.settingsGraph
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -338,7 +338,16 @@ fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerVie
val provider =
entryProvider<NavKey> {
contactsGraph(backStack, uIViewModel.scrollToTopEventFlow)
nodesGraph(backStack, uIViewModel.scrollToTopEventFlow)
nodesGraph(
backStack = backStack,
scrollToTopEvents = uIViewModel.scrollToTopEventFlow,
nodeMapScreen = { destNum, onNavigateUp ->
val vm =
org.koin.compose.viewmodel.koinViewModel<org.meshtastic.feature.map.node.NodeMapViewModel>()
vm.setDestNum(destNum)
org.meshtastic.app.map.node.NodeMapScreen(vm, onNavigateUp = onNavigateUp)
},
)
mapGraph(backStack)
channelsGraph(backStack)
connectionsGraph(backStack)

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.ui
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import kotlinx.coroutines.flow.emptyFlow
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.feature.connections.navigation.connectionsGraph
import org.meshtastic.feature.firmware.navigation.firmwareGraph
import org.meshtastic.feature.map.navigation.mapGraph
import org.meshtastic.feature.messaging.navigation.contactsGraph
import org.meshtastic.feature.node.navigation.nodesGraph
import org.meshtastic.feature.settings.navigation.channelsGraph
import org.meshtastic.feature.settings.navigation.settingsGraph
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class NavigationAssemblyTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
fun verifyNavigationGraphsAssembleWithoutCrashing() {
composeTestRule.setContent {
val backStack = rememberNavBackStack(NodesRoutes.NodesGraph)
entryProvider<NavKey> {
contactsGraph(backStack, emptyFlow())
nodesGraph(backStack = backStack, scrollToTopEvents = emptyFlow(), nodeMapScreen = { _, _ -> })
mapGraph(backStack)
channelsGraph(backStack)
connectionsGraph(backStack)
settingsGraph(backStack)
firmwareGraph(backStack)
}
}
}
}

View file

@ -0,0 +1,5 @@
# Track extract_android_navigation_20260318 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View file

@ -0,0 +1,8 @@
{
"track_id": "extract_android_navigation_20260318",
"type": "refactor",
"status": "new",
"created_at": "2026-03-18T00:00:00Z",
"updated_at": "2026-03-18T00:00:00Z",
"description": "Extract Android Navigation graphs to feature modules for app thinning"
}

View file

@ -0,0 +1,33 @@
# Implementation Plan: Extract Android Navigation
## Phase 1: Preparation & Base Module Abstraction [checkpoint: 421a587]
- [x] Task: Review current navigation graph assembly in `app/src/main/kotlin/org/meshtastic/app/navigation/`.
- [x] Identify dependencies between feature navigation graphs and core routing definitions.
- [x] Create missing directory structures in feature modules' `androidMain/kotlin/org/meshtastic/feature/*/navigation` if they don't exist.
- [x] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Base Module Abstraction' (Protocol in workflow.md)
## Phase 2: Feature Module Extraction [checkpoint: 9a27cce]
- [x] Task: Extract Settings Navigation.
- [x] Move `SettingsNavigation.kt` to `feature:settings/androidMain`.
- [x] Fix package declarations and broken imports.
- [x] Task: Extract Nodes & Connections Navigation.
- [x] Move `NodesNavigation.kt` to `feature:node/androidMain`.
- [x] Move `ConnectionsNavigation.kt` to `feature:connections/androidMain`.
- [x] Fix package declarations and broken imports.
- [x] Task: Extract Messaging & Remaining Navigation.
- [x] Move `ContactsNavigation.kt` to `feature:messaging/androidMain`.
- [x] Move `ChannelsNavigation.kt` to `feature:settings/androidMain` or `feature:node`.
- [x] Move `FirmwareNavigation.kt` to `feature:firmware/androidMain`.
- [x] Move `MapNavigation.kt` to `feature:map/androidMain`.
- [x] Fix package declarations and broken imports.
- [x] Task: Conductor - User Manual Verification 'Phase 2: Feature Module Extraction' (Protocol in workflow.md)
## Phase 3: Root Assembly & Testing [checkpoint: a1e9da3]
- [x] Task: Refactor Root App Graph.
- [x] Update root composition to import the newly relocated navigation extension functions.
- [x] Remove any leftover navigation wiring from the `app` module.
- [x] Task: Implement Navigation Assembly Tests.
- [x] Add basic Android instrumented or Roboelectric tests in `:app` to verify that the `NavHost` successfully constructs all feature graphs without crashing.
- [x] Task: Review previous steps and update project documentation.
- [x] Update `conductor/tech-stack.md` and `conductor/product.md` if necessary to reflect the thinned app module and JetBrains Navigation 3 common usage.
- [x] Task: Conductor - User Manual Verification 'Phase 3: Root Assembly & Testing' (Protocol in workflow.md)

View file

@ -0,0 +1,19 @@
# Specification: Extract Android Navigation graphs to feature modules for app thinning
## Overview
The primary goal of this track is to thin out the app module by moving the Android-specific navigation graph wiring (e.g., SettingsNavigation.kt, NodesNavigation.kt, ConnectionsNavigation.kt) into their respective feature modules (e.g., feature:settings, feature:node, feature:connections). This aligns the Android implementation with the Desktop application's architecture, where navigation logic is collocated with the features it routes.
## Functional Requirements
- **Target Modules:** Move all feature-specific navigation files from `app/src/main/kotlin/org/meshtastic/app/navigation/` to the `androidMain` source sets of their corresponding `feature:*` modules.
- **Architecture:** Implement JetBrains Navigation 3 best practices for common usage across KMP modules. This involves ensuring the feature modules expose their navigation graphs seamlessly to the root NavHost in the app module, minimizing tight coupling.
- **Root App Shell:** The app module should only retain the root MainActivity, the root DI graph assembly, and the top-level NavHost (e.g., MeshtasticApp.kt or similar entry point), calling into the feature modules' exposed graph functions.
## Non-Functional Requirements
- **Testability:** Add or update tests to verify that the complete navigation graph is correctly assembled from the individual feature modules without errors.
- **Maintainability:** The extraction must preserve all existing deep links, arguments, and navigation transitions currently defined in the Android app.
## Acceptance Criteria
- [ ] The `app/src/main/kotlin/org/meshtastic/app/navigation/` directory only contains the root graph assembly.
- [ ] All Android feature navigation graphs are successfully extracted to their respective `feature:*` modules.
- [ ] The Android app compiles and runs successfully, with all navigation flows working identically to the previous implementation.
- [ ] New graph assembly tests are added and pass in CI/local environments.

View file

@ -0,0 +1,5 @@
# Track extract_remaining_background_20260318 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)

View file

@ -0,0 +1,8 @@
{
"track_id": "extract_remaining_background_20260318",
"type": "refactor",
"status": "new",
"created_at": "2026-03-18T14:55:00Z",
"updated_at": "2026-03-18T14:55:00Z",
"description": "Extract remaining background services and workers from app module"
}

View file

@ -0,0 +1,29 @@
# Implementation Plan: Extract remaining background services and workers from app module
## Phase 1: Preparation & Location Manager Abstraction [checkpoint: 57052fc]
- [x] Task: Review current implementations in `app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt` and `app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt`.
- [x] Task: Create KMP shared interface or base class in `core:service/commonMain` for the Location Manager if applicable, aligning with KMP best practices.
- [x] Task: Relocate `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` to `core:service/src/androidMain/...`.
- [x] Task: Update package declarations and resolve broken imports in the app module.
- [x] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Location Manager Abstraction' (Protocol in workflow.md)
## Phase 2: Message Queue Abstraction [checkpoint: dda10b4]
- [x] Task: Review `app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt`.
- [x] Task: Identify opportunities to extract non-Android specific queue logic to `feature:messaging/commonMain`.
- [x] Task: Relocate `WorkManagerMessageQueue.kt` to `feature:messaging/src/androidMain/...`.
- [x] Task: Update package declarations and resolve broken imports.
- [x] Task: Conductor - User Manual Verification 'Phase 2: Message Queue Abstraction' (Protocol in workflow.md)
## Phase 3: Widget Extraction [checkpoint: 0c027e3]
- [x] Task: Review the contents of `app/src/main/kotlin/org/meshtastic/app/widget/`.
- [x] Task: Decide whether to move widgets to an existing module (e.g. `core:ui` or `feature:node`) or create a new `feature:widget` module.
- [x] Task: Relocate `LocalStatsWidget.kt`, `LocalStatsWidgetReceiver.kt`, `LocalStatsWidgetState.kt`, `RefreshLocalStatsAction.kt`, and `AndroidAppWidgetUpdater.kt`.
- [x] Task: Relocate necessary widget resources, strings, and AndroidManifest declarations.
- [x] Task: Conductor - User Manual Verification 'Phase 3: Widget Extraction' (Protocol in workflow.md)
## Phase 4: Dependency Injection Refactoring [checkpoint: c5f09dc]
- [x] Task: Review `app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt` and `di/AppKoinModule.kt`.
- [x] Task: Move DI bindings for the relocated classes to their new respective modules (e.g., `ServiceKoinModule`, `MessagingKoinModule`).
- [x] Task: Ensure the root app module's DI configuration successfully includes the feature and core Koin modules.
- [x] Task: Run Android instrumented/unit tests to verify graph compilation.
- [x] Task: Conductor - User Manual Verification 'Phase 4: Dependency Injection Refactoring' (Protocol in workflow.md)

View file

@ -0,0 +1,22 @@
# Specification: Extract remaining background services and workers from app module
## Overview
The primary goal of this track is to continue the app module thinning effort by extracting the remaining Android-specific background services, workers, and widgets from the `app` module into appropriate core or feature modules. Where possible, business logic from these components should be abstracted and moved to `commonMain` to support KMP targets. This will leave the app module as a thin entry point shell.
## Functional Requirements
- **Core Services:** Extract `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` to `core:service/androidMain`. Refactor underlying logic to `core:service/commonMain` where applicable.
- **Messaging Workers:** Extract `WorkManagerMessageQueue.kt` to `feature:messaging/androidMain`. Analyze logic for potential `commonMain` abstraction.
- **Widgets:** Extract the `LocalStatsWidget` implementation to a new or existing appropriate feature module (e.g. `feature:widget/androidMain`) following KMP feature module conventions.
- **Dependency Injection:** Update the DI graph (`MainKoinModule.kt` / `AppKoinModule.kt`) to resolve these implementations from their new module locations using Koin compiler plugin annotations where applicable.
## Non-Functional Requirements
- **Testability:** Existing tests related to these services and workers should pass after relocation.
- **Maintainability:** The extraction must preserve all existing app functionality, including background synchronization, location tracking, and widget updates.
## Acceptance Criteria
- [ ] `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` are successfully moved to `core:service`.
- [ ] `WorkManagerMessageQueue.kt` is successfully moved to `feature:messaging`.
- [ ] App Widgets are extracted out of the `app` module into an appropriate feature module.
- [ ] Any logic that can be abstracted to `commonMain` has been extracted and shared.
- [ ] `MainKoinModule.kt` is refactored, and DI wires everything correctly.
- [ ] The Android app compiles and runs successfully, with background tasks and widgets working identically to the previous implementation.

View file

@ -22,4 +22,4 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application designed to facil
## Key Architecture Goals
- Provide a robust, shared KMP core (`core:model`, `core:ble`, `core:repository`, `core:domain`, `core:data`, `core:network`, `core:service`) to support multiple platforms (Android, Desktop, iOS)
- Ensure offline-first functionality and resilient data persistence (Room KMP)
- Decouple UI logic into shared components (`core:ui`, `feature:*`) using Compose Multiplatform
- Decouple UI and navigation logic into shared feature modules (`core:ui`, `feature:*`) using Compose Multiplatform

View file

@ -12,7 +12,7 @@
## Architecture
- **MVI / Unidirectional Data Flow:** Shared view models using the multiplatform `androidx.lifecycle.ViewModel`.
- **JetBrains Navigation 3:** Multiplatform fork for state-based, compose-first navigation without relying on `NavController`.
- **JetBrains Navigation 3:** Multiplatform fork for state-based, compose-first navigation without relying on `NavController`. Navigation graphs are decoupled and extracted into their respective `feature:*` modules, allowing a thinned out root `app` module.
## Dependency Injection
- **Koin 4.2:** Leverages Koin Annotations and the K2 Compiler Plugin for pure compile-time DI, completely replacing Hilt.

View file

@ -3,6 +3,6 @@
This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.
---
- [ ] **Track: Expand Testing Coverage**
*Link: [./tracks/expand_testing_20260318/](./tracks/expand_testing_20260318/)*
*Link: [./tracks/expand_testing_20260318/](./tracks/expand_testing_20260318/)*

View file

@ -12,12 +12,16 @@ This is the public android API for talking to meshtastic radios.
To connect to meshtastic you should bind to it per https://developer.android.com/guide/components/bound-services
The intent you use to reach the service should look like this:
The intent you use to reach the service should ideally use the action string:
val intent = Intent("com.geeksville.mesh.Service")
Or if using an explicit intent:
val intent = Intent().apply {
setClassName(
"com.geeksville.mesh",
"com.geeksville.mesh.service.MeshService"
"org.meshtastic.core.service.MeshService"
)
}

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.service
package org.meshtastic.core.service
import android.annotation.SuppressLint
import android.app.Application

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app
package org.meshtastic.core.service
import android.content.Context
import android.content.Context.BIND_ABOVE_CLIENT
@ -26,12 +26,6 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory
import org.meshtastic.core.common.util.SequentialJob
import org.meshtastic.core.service.AndroidServiceRepository
import org.meshtastic.core.service.BindFailedException
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.MeshService
import org.meshtastic.core.service.ServiceClient
import org.meshtastic.core.service.startService
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
@Factory

View file

@ -6,8 +6,7 @@ This document captures discoverable patterns that are already used in the reposi
- Keep domain logic in KMP modules (`commonMain`) and keep Android framework wiring in `app` or `androidMain`.
- Use `core:*` for shared logic, `feature:*` for user-facing flows, and `app` for Android entrypoints and integration wiring.
- Example: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt` contains shared ViewModel logic, while `app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt` provides an Android/Koin wrapper for platform-specific functionality (CSV export via `android.net.Uri`).
- Note: Many former passthrough wrappers have been eliminated. Only ViewModels with genuine Android-specific logic (file I/O, permissions, `Locale`-aware formatting) retain wrappers in `app/`.
- Note: Former passthrough Android ViewModel wrappers have been eliminated. ViewModels are now shared KMP components. Platform-specific dependencies (file I/O, permissions) are isolated behind injected `core:repository` interfaces.
## 2) Dependency injection conventions (Koin)
@ -20,7 +19,7 @@ This document captures discoverable patterns that are already used in the reposi
## 3) Navigation conventions (Navigation 3)
- Use Navigation 3 types (`NavKey`, `NavBackStack`, entry providers) instead of legacy controller-first patterns.
- Example graph using `EntryProviderScope<NavKey>` and `backStack.add/removeLastOrNull`: `app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt`.
- Example graph using `EntryProviderScope<NavKey>` and `backStack.add/removeLastOrNull`: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`.
- Example feature flow using `rememberNavBackStack` and `NavDisplay<NavKey>`: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt`.
## 4) UI and resources

View file

@ -22,7 +22,6 @@ Version note: align guidance with repository-pinned versions in `gradle/libs.ver
- App-level module scanning: `app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt`
- App startup + Koin init: `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
- Android wrapper ViewModel pattern: `app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt`
- Shared ViewModel base: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt`
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/BaseUIViewModel.kt`
@ -39,7 +38,7 @@ Version note: align guidance with repository-pinned versions in `gradle/libs.ver
- Typed routes: `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
- App root backstack + `NavDisplay`: `app/src/main/kotlin/org/meshtastic/app/ui/Main.kt`
- Graph entry provider pattern: `app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt`
- Graph entry provider pattern: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Feature-level Navigation 3 usage: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt`
- Desktop Navigation 3 shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
- Desktop nav graph entries: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt`

View file

@ -19,14 +19,12 @@ Reference examples:
1. Implement or extend base ViewModel logic in `feature/<name>/src/commonMain/...`.
2. Keep shared class free of Android framework dependencies.
3. Keep Android framework dependencies out of shared logic; if the module already uses Koin annotations in `commonMain`, keep patterns consistent and ensure app root inclusion.
4. Add/update Android wrapper in `app/src/main/kotlin/org/meshtastic/app/...` with `@KoinViewModel` when Android instantiation is needed.
5. Update navigation entry points in `app/src/main/kotlin/org/meshtastic/app/navigation/...` to resolve wrapper ViewModels with `koinViewModel()`.
4. Update navigation entry points in `feature/*/src/androidMain/kotlin/org/meshtastic/feature/*/navigation/...` to resolve ViewModels with `koinViewModel()`.
Reference examples:
- Shared base: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt`
- Android wrapper (remaining): `app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt`
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/BaseUIViewModel.kt`
- Navigation usage: `app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt`
- Navigation usage: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Desktop navigation usage: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt`
## Playbook C: Add a new dependency or service binding
@ -45,12 +43,12 @@ Reference examples:
1. Define/extend route keys in `core:navigation`.
2. Implement feature entry/content using Navigation 3 types (`NavKey`, `NavBackStack`, `EntryProviderScope`).
3. Add graph entries under `app/src/main/kotlin/org/meshtastic/app/navigation`.
3. Add graph entries under the relevant feature module's `navigation` package (e.g., `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation`).
4. Use backstack mutation (`add`, `removeLastOrNull`) instead of introducing controller-coupled APIs.
5. Verify deep-link behavior if route is externally reachable.
Reference examples:
- App graph wiring: `app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt`
- App graph wiring: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Feature intro graph pattern: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt`
- Desktop nav shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
- Desktop nav graph entries: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt`

View file

@ -11,7 +11,7 @@ The codebase is **~98% structurally KMP** — 18/20 core modules and 7/7 feature
Of the five structural gaps originally identified, four are resolved and one remains in progress:
1. **`app` is a God module** — originally 90 files / ~11K LOC of transport, service, UI, and ViewModel code that should live in core/feature modules. *(In progress — connections extracted, ChannelViewModel/NodeMapViewModel/NodeContextMenu/EmptyDetailPlaceholder moved to shared modules, currently 63 files)*
1. **`app` is a God module** — originally 90 files / ~11K LOC of transport, service, UI, and ViewModel code that should live in core/feature modules. *(✅ Resolved — app module reduced to 6 files: `MainActivity`, `MeshUtilApplication`, Nav shell, and DI config)*
2. ~~**Radio transport layer is app-locked**~~ — ✅ Resolved: `RadioTransport` interface in `core:repository/commonMain`; shared `StreamFrameCodec` + `TcpTransport` in `core:network`.
3. ~~**`java.*` APIs leak into `commonMain`**~~ — ✅ Resolved: `Locale`, `ConcurrentHashMap`, `ReentrantLock` purged.
4. ~~**Zero feature-level `commonTest`**~~ — ✅ Resolved: 131 shared tests across all 7 features; `core:testing` module established.
@ -24,7 +24,7 @@ Of the five structural gaps originally identified, four are resolved and one rem
| `core/*/commonMain` | 337 | 32,700 | Shared business/data logic |
| `feature/*/commonMain` | 146 | 19,700 | Shared feature UI + ViewModels |
| `feature/*/androidMain` | 62 | 14,700 | Platform UI (charts, previews, permissions) |
| `app/src/main` | 63 | ~9,500 | Android app shell (target: ~20 files) |
| `app/src/main` | 6 | ~300 | Android app shell (target achieved) |
| `desktop/src` | 26 | 4,800 | Desktop app shell |
| `core/*/androidMain` | 49 | 3,500 | Platform implementations |
| `core/*/jvmMain` | 11 | ~500 | JVM actuals |
@ -38,16 +38,16 @@ Of the five structural gaps originally identified, four are resolved and one rem
### A1. `app` module is a God module
The `app` module should be a thin shell (~20 files): `MainActivity`, DI assembly, nav host. Originally it held **90 files / ~11K LOC**, now reduced to **63 files / ~9.5K LOC**:
The `app` module should be a thin shell (~20 files): `MainActivity`, DI assembly, nav host. Originally it held **90 files / ~11K LOC**, now completely reduced to a **6-file shell**:
| Area | Files | LOC | Where it should live |
|---|---:|---:|---|
| `repository/radio/` | 22 | ~2,000 | `core:service` / `core:network` |
| `service/` | 12 | ~1,500 | `core:service/androidMain` |
| `navigation/` | 7 | ~720 | Stay in `app` (Nav 3 host wiring) |
| `service/` | 12 | ~1,500 | Extracted to `core:service/androidMain` |
| `navigation/` | ~1 | ~200 | Root Nav 3 host wiring stays in `app`. Feature graphs moved to `feature:*`. |
| `settings/` ViewModels | 3 | ~350 | Thin Android wrappers (genuine platform deps) |
| `widget/` | 4 | ~300 | Stay in `app` (Glance is Android-only) |
| `worker/` | 4 | ~350 | `core:service/androidMain` |
| `widget/` | 4 | ~300 | Extracted to `feature:widget` |
| `worker/` | 4 | ~350 | Extracted to `core:service/androidMain` and `feature:messaging/androidMain` |
| DI + Application + MainActivity | 5 | ~500 | Stay in `app` ✓ |
| UI screens + ViewModels | 5 | ~1,200 | Stay in `app` (Android-specific deps) |
@ -204,7 +204,7 @@ Ordered by impact × effort:
|---|---:|---:|---|
| Shared business/data logic | 8.5/10 | **9/10** | RadioTransport interface unified; all core layers shared |
| Shared feature/UI logic | 9.5/10 | **8.5/10** | All 7 KMP features; connections unified; Vico charts in commonMain |
| Android decoupling | 8.5/10 | **8/10** | Connections extracted; GMS purged; ChannelViewModel/NodeMapViewModel/NodeContextMenu extracted; app 63→target 20 files |
| Android decoupling | 8.5/10 | **9/10** | Connections, Navigation, Services, & Widgets extracted; GMS purged; app ~40->target 20 files |
| Multi-target readiness | 8/10 | **8/10** | Full JVM; release-ready desktop; iOS not declared |
| CI confidence | 8.5/10 | **9/10** | 25 modules validated; feature:connections + desktop in CI; native release installers |
| DI portability | 7/10 | **8/10** | Koin annotations in commonMain; supportedDeviceTypes injected per platform |

View file

@ -148,7 +148,7 @@ Adopt a **hybrid parity model**:
- Shared routes: `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
- Android shell: `app/src/main/kotlin/org/meshtastic/app/ui/Main.kt`
- Android graph registrations: `app/src/main/kotlin/org/meshtastic/app/navigation/`
- Android graph registrations: `feature/*/src/androidMain/kotlin/org/meshtastic/feature/*/navigation/`
- Desktop shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
- Desktop graph registrations: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/`

View file

@ -72,7 +72,7 @@ Working Compose Desktop application with:
|---|---|---|
| Shared business/data logic | **9/10** | All core layers shared; RadioTransport interface unified |
| Shared feature/UI logic | **8.5/10** | All 7 KMP; feature:connections unified with dynamic transport detection |
| Android decoupling | **8/10** | No known `java.*` calls in `commonMain`; app module extraction in progress |
| Android decoupling | **9/10** | No known `java.*` calls in `commonMain`; app module extraction in progress (navigation, connections, background services, and widgets extracted) |
| Multi-target readiness | **8/10** | Full JVM; release-ready desktop; iOS not declared |
| CI confidence | **9/10** | 25 modules validated (including feature:connections); native release installers automated |
| DI portability | **8/10** | Koin annotations in commonMain; supportedDeviceTypes injected per platform |
@ -84,9 +84,9 @@ Working Compose Desktop application with:
| Lens | % |
|---|---:|
| Android-first structural KMP | ~98% |
| Shared business logic | ~95% |
| Shared feature/UI | ~90% |
| Android-first structural KMP | ~100% |
| Shared business logic | ~98% |
| Shared feature/UI | ~95% |
| True multi-target readiness | ~75% |
| "Add iOS without surprises" | ~65% |
@ -118,6 +118,7 @@ Based on the latest codebase investigation, the following steps are proposed to
- Both shells iterate `TopLevelDestination.entries` with shared icon mapping from `core:ui` (`TopLevelDestinationExt.icon`).
- Desktop locale changes now trigger a full subtree recomposition from `Main.kt` without resetting the shared Navigation 3 backstack, so translated labels update in place.
- Firmware remains available as an in-flow route instead of a top-level destination, matching Android information architecture.
- Android navigation graphs are decoupled and extracted into their respective feature modules, aligning with the Desktop architecture.
- Parity tests exist in `core:navigation/commonTest` (`NavigationParityTest`) and `desktop/test` (`DesktopTopLevelDestinationParityTest`).
- Remaining parity work is documented in [`decisions/navigation3-parity-2026-03.md`](./decisions/navigation3-parity-2026-03.md): serializer registration validation and platform exception tracking.
@ -132,19 +133,16 @@ Extracted to shared `commonMain` (no longer app-only):
- `MetricsViewModel``feature:node/commonMain`
- `UIViewModel``core:ui/commonMain`
- `ChannelViewModel``feature:settings/commonMain`
- `NodeMapViewModel``feature:map/commonMain`
- `NodeMapViewModel``feature:map/commonMain` (Shared logic for node-specific maps)
- `BaseMapViewModel``feature:map/commonMain` (Core contract for all maps)
Extracted to core KMP modules (Android-specific implementations):
- Android Services, WorkManager Workers, and BroadcastReceivers → `core:service/androidMain`
- BLE, USB/Serial, TCP radio connections, and NsdManager → `core:network/androidMain`
Remaining to be extracted from `:app` to achieve a true thin-shell module:
- Navigation routes (`ChannelsNavigation.kt`, `SettingsNavigation.kt`, etc.)
- Android App Widgets (`LocalStatsWidget.kt`, `AndroidAppWidgetUpdater.kt`)
- Message Queue implementation (`WorkManagerMessageQueue.kt`)
- Location provider bindings (`AndroidMeshLocationManager.kt`)
- Top-level UI composition (`ui/Main.kt`, `ui/node/AdaptiveNodeListScreen.kt`)
- Root Activity and Koin bootstrapping (`MainActivity.kt`, `MeshUtilApplication.kt`, `MeshServiceClient.kt`)
Remaining to be extracted from `:app` or unified in `commonMain`:
- `MapViewModel` (Unify Google/F-Droid flavors into a single `commonMain` class consuming a `MapConfigProvider` interface)
- Top-level UI composition (`ui/Main.kt`)
## Prerelease Dependencies

View file

@ -78,39 +78,27 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low |
## Near-Term Priorities (30 days)
1. **`core:testing` module** — ✅ Done (established shared fakes for cross-module `commonTest`)
2. **Feature `commonTest` bootstrap** — ✅ Done (131 shared tests across all 7 features covering integration and error handling)
3. **Radio transport abstraction** — ✅ Done: Defined `RadioTransport` interface in `core:repository/commonMain` and replaced `IRadioInterface`; Next: continue extracting remaining platform transports from `app/repository/radio/` into core modules
4. **`feature:connections` module** — ✅ Done: Extracted connections UI into KMP feature module with dynamic transport availability detection
5. **Navigation 3 parity baseline** — ✅ Done: shared `TopLevelDestination` in `core:navigation`; both shells use same enum; parity tests in `core:navigation/commonTest` and `desktop/test`
6. **iOS CI gate** — add `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI (compile-only, no implementations)
7. **Build-logic consolidation** — ✅ Done: Created `meshtastic.kmp.feature` convention plugin (modelled after NiA's `AndroidFeatureImplConventionPlugin`). Composes `kmp.library` + `kmp.library.compose` + `koin` and wires common Compose/Lifecycle/Koin/androidMain deps. All 7 feature modules migrated; ~100 duplicated dep lines eliminated.
1. **Evaluate KMP-native testing tools** — Evaluate `Mokkery` or `Mockative` to replace `mockk` in `commonMain` of `core:testing` for iOS readiness. Integrate `Turbine` for shared `Flow` testing.
2. **Desktop Map Integration** — Address the major Desktop feature gap by implementing a raster map view using [**MapComposeMP**](https://github.com/p-lr/MapComposeMP).
- Implement a `MapComposeProvider` for Desktop.
- Implement a **Web Mercator Projection** helper in `feature:map/commonMain` to translate GPS coordinates to the 2D image plane.
- Leverage the existing `BaseMapViewModel` contract.
3. **Unify `MapViewModel`** — Collapse the remaining Google and F-Droid specific `MapViewModel` classes in the `:app` module into a single `commonMain` implementation by isolating platform-specific settings (styles, tile sources) behind a repository interface.
4. **iOS CI gate** — add `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI (compile-only, no implementations) to ensure `commonMain` remains pure.
## Medium-Term Priorities (60 days)
1. **App module thinning** — Extracted ChannelViewModel, NodeMapViewModel, NodeContextMenu, EmptyDetailPlaceholder to shared modules.
- ✅ **Done:** Extracted remaining 5 ViewModels: `SettingsViewModel`, `RadioConfigViewModel`, `DebugViewModel`, `MetricsViewModel`, `UIViewModel` to shared KMP modules.
- ✅ **Done:** Extracted service, worker, and radio files from `app` to `core:service/androidMain` and `core:network/androidMain`.
- **Next:** Extract remaining Android-specific files (e.g., Navigation files, App Widgets, message queues, and root Activity logic) out of `:app` to establish a truly thin app module.
2. ✅ **Done:** **Serial/USB transport** — direct radio connection on Desktop via jSerialComm
3. **MQTT transport** — cloud relay operation (KMP, benefits all targets) ✅
4. **Evaluate KMP-native testing tools** — Evaluate `Mokkery` or `Mockative` to replace `mockk` in `commonMain` of `core:testing` for iOS readiness. Integrate `Turbine` for shared `Flow` testing.
5. **Desktop ViewModel auto-wiring** — ✅ Done: ensured Koin K2 Compiler Plugin generates ViewModel modules for JVM target; eliminated manual wiring in `DesktopKoinModule`
5. **KMP charting** — ✅ Done: Vico charts migrated to `feature:node/commonMain` using KMP artifacts; desktop wires them directly
6. **Navigation contract extraction** — ✅ Done: shared `TopLevelDestination` enum in `core:navigation`; icon mapping in `core:ui`; parity tests in place. Both shells derive from the same source of truth.
7. **Dependency stabilization** — track stable releases for CMP, Koin, Lifecycle, Nav3
1. **iOS proof target** — Begin stubbing iOS target implementations (`NoopStubs.kt` equivalent) and setup an Xcode skeleton project.
2. **`core:api` contract split** — separate transport-neutral service contracts from the Android AIDL packaging to support iOS/Desktop service layers.
3. **Decouple Firmware DFU**`feature:firmware` relies on Android-only DFU libraries. Evaluate wrapping this in a shared KMP interface or extracting it to allow the core `feature:firmware` module to be utilized on desktop/iOS.
## Longer-Term (90+ days)
1. **iOS proof target** — declare `iosArm64()`/`iosSimulatorArm64()` in KMP modules; BLE via Kable/CoreBluetooth
2. **Platform-Native UI Interop**
1. **Platform-Native UI Interop**
- **iOS Maps & Camera:** Implement `MapLibre` or `MKMapView` via Compose Multiplatform's `UIKitView`. Leverage `AVCaptureSession` wrapped in `UIKitView` to fulfill the `LocalBarcodeScannerProvider` contract.
- **Desktop Maps:** Implement maps via `SwingPanel` wrapper, utilizing experimental interop blending (`compose.interop.blending=true`) to ensure tooltips and Compose overlays render correctly on top of the native JComponent.
- **Web (wasmJs) Integrations:** Leverage `HtmlView` to embed raw DOM elements (e.g., `<video>`, `<iframe>`, or canvas-based maps) directly into the Compose UI tree while binding the root app via `CanvasBasedWindow`.
3. **`core:api` contract split** — separate transport-neutral service contracts from Android AIDL packaging
4. **Native packaging** — ✅ Done: DMG, MSI, DEB distributions for Desktop via release pipeline
5. **Module maturity dashboard** — living inventory of per-module KMP readiness
6. **Shared UI vs Shared Logic split** — If the iOS target utilizes native SwiftUI instead of Compose Multiplatform, evaluate splitting feature modules into pure `sharedLogic` (business rules, ViewModels) and `sharedUI` (Compose Multiplatform) to prevent dragging Compose dependencies into pure native iOS apps.
2. **Module maturity dashboard** — living inventory of per-module KMP readiness.
3. **Shared UI vs Shared Logic split** — If the iOS target utilizes native SwiftUI instead of Compose Multiplatform, evaluate splitting feature modules into pure `sharedLogic` (business rules, ViewModels) and `sharedUI` (Compose Multiplatform) to prevent dragging Compose dependencies into pure native iOS apps.
## Design Principles

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.navigation
package org.meshtastic.feature.connections.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack

View file

@ -32,6 +32,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
implementation(projects.core.ble)
implementation(projects.core.common)
implementation(projects.core.data)

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.navigation
package org.meshtastic.feature.firmware.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack

View file

@ -31,6 +31,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.database)

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.navigation
package org.meshtastic.feature.map.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.navigation
package org.meshtastic.feature.messaging.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.messaging.domain.worker
package org.meshtastic.feature.messaging.worker
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.ui.node
package org.meshtastic.feature.node.navigation
import androidx.activity.compose.BackHandler
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.navigation
package org.meshtastic.feature.node.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CellTower
@ -35,8 +35,6 @@ import kotlinx.coroutines.flow.Flow
import org.jetbrains.compose.resources.StringResource
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf
import org.meshtastic.app.map.node.NodeMapScreen
import org.meshtastic.app.ui.node.AdaptiveNodeListScreen
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.NodesRoutes
@ -52,7 +50,6 @@ import org.meshtastic.core.resources.power
import org.meshtastic.core.resources.signal
import org.meshtastic.core.resources.traceroute
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.feature.map.node.NodeMapViewModel
import org.meshtastic.feature.node.metrics.DeviceMetricsScreen
import org.meshtastic.feature.node.metrics.EnvironmentMetricsScreen
import org.meshtastic.feature.node.metrics.HostMetricsLogScreen
@ -66,7 +63,11 @@ import org.meshtastic.feature.node.metrics.TracerouteLogScreen
import org.meshtastic.feature.node.metrics.TracerouteMapScreen
import kotlin.reflect.KClass
fun EntryProviderScope<NavKey>.nodesGraph(backStack: NavBackStack<NavKey>, scrollToTopEvents: Flow<ScrollToTopEvent>) {
fun EntryProviderScope<NavKey>.nodesGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
nodeMapScreen: @Composable (destNum: Int, onNavigateUp: () -> Unit) -> Unit,
) {
entry<NodesRoutes.NodesGraph> {
AdaptiveNodeListScreen(
backStack = backStack,
@ -83,13 +84,14 @@ fun EntryProviderScope<NavKey>.nodesGraph(backStack: NavBackStack<NavKey>, scrol
)
}
nodeDetailGraph(backStack, scrollToTopEvents)
nodeDetailGraph(backStack, scrollToTopEvents, nodeMapScreen)
}
@Suppress("LongMethod")
fun EntryProviderScope<NavKey>.nodeDetailGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
nodeMapScreen: @Composable (destNum: Int, onNavigateUp: () -> Unit) -> Unit,
) {
entry<NodesRoutes.NodeDetailGraph> { args ->
AdaptiveNodeListScreen(
@ -109,11 +111,7 @@ fun EntryProviderScope<NavKey>.nodeDetailGraph(
)
}
entry<NodeDetailRoutes.NodeMap> { args ->
val vm = koinViewModel<NodeMapViewModel>()
vm.setDestNum(args.destNum)
NodeMapScreen(vm, onNavigateUp = { backStack.removeLastOrNull() })
}
entry<NodeDetailRoutes.NodeMap> { args -> nodeMapScreen(args.destNum) { backStack.removeLastOrNull() } }
entry<NodeDetailRoutes.TracerouteLog> { args ->
val metricsViewModel =

View file

@ -47,6 +47,8 @@ kotlin {
implementation(libs.kotlinx.collections.immutable)
implementation(libs.aboutlibraries.compose.m3)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
}
androidMain.dependencies {

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.ui.sharing
package org.meshtastic.feature.settings.navigation
import android.os.RemoteException
import androidx.compose.foundation.border
@ -96,8 +96,6 @@ import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.core.ui.util.generateQrCode
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.settings.channel.ChannelViewModel
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
import org.meshtastic.proto.ChannelSet

View file

@ -14,13 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.navigation
package org.meshtastic.feature.settings.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.ui.sharing.ChannelScreen
import org.meshtastic.core.navigation.ChannelsRoutes
import org.meshtastic.feature.settings.radio.RadioConfigViewModel

View file

@ -16,7 +16,7 @@
*/
@file:Suppress("Wrapping", "SpacingAroundColon")
package org.meshtastic.app.navigation
package org.meshtastic.feature.settings.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -39,8 +39,6 @@ import org.meshtastic.feature.settings.debugging.DebugScreen
import org.meshtastic.feature.settings.debugging.DebugViewModel
import org.meshtastic.feature.settings.filter.FilterSettingsScreen
import org.meshtastic.feature.settings.filter.FilterSettingsViewModel
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.ModuleRoute
import org.meshtastic.feature.settings.radio.CleanNodeDatabaseScreen
import org.meshtastic.feature.settings.radio.CleanNodeDatabaseViewModel
import org.meshtastic.feature.settings.radio.RadioConfigViewModel

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.android.library.compose)
alias(libs.plugins.meshtastic.koin)
}
android {
namespace = "org.meshtastic.feature.widget"
defaultConfig { minSdk = 26 }
}
dependencies {
implementation(projects.core.common)
implementation(projects.core.model)
implementation(projects.core.resources)
implementation(projects.core.repository)
implementation(libs.androidx.glance.appwidget)
implementation(libs.androidx.glance.material3)
implementation(libs.androidx.glance.preview)
implementation(libs.androidx.glance.appwidget.preview)
implementation(libs.compose.multiplatform.resources)
implementation(libs.kermit)
implementation(libs.koin.annotations)
}

View file

@ -14,12 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.service
package org.meshtastic.feature.widget
import android.content.Context
import androidx.glance.appwidget.updateAll
import co.touchlab.kermit.Logger
import org.koin.core.annotation.Single
import org.meshtastic.app.widget.LocalStatsWidget
import org.meshtastic.core.repository.AppWidgetUpdater
@Single

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.widget
package org.meshtastic.feature.widget
import android.annotation.SuppressLint
import android.content.Context
@ -132,11 +132,11 @@ class LocalStatsWidget :
Scaffold(
titleBar = {
TitleBar(
startIcon = ImageProvider(org.meshtastic.app.R.drawable.app_icon),
startIcon = ImageProvider(R.drawable.app_icon),
title = stringResource(Res.string.meshtastic_app_name),
actions = {
CircleIconButton(
imageProvider = ImageProvider(org.meshtastic.app.R.drawable.ic_refresh),
imageProvider = ImageProvider(R.drawable.ic_refresh),
contentDescription = stringResource(Res.string.refresh),
onClick = actionRunCallback<RefreshLocalStatsAction>(),
backgroundColor = null,
@ -145,7 +145,15 @@ class LocalStatsWidget :
)
},
modifier =
GlanceModifier.fillMaxSize().clickable(actionStartActivity<org.meshtastic.app.MainActivity>()),
GlanceModifier.fillMaxSize()
.clickable(
actionStartActivity(
android.content.ComponentName(
context.packageName,
"org.meshtastic.app.MainActivity",
),
),
),
) {
if (state.showContent) {
FullStatsContent(state)
@ -289,7 +297,7 @@ class LocalStatsWidget :
CircularProgressIndicator(modifier = GlanceModifier.size(24.dp))
} else {
Image(
provider = ImageProvider(org.meshtastic.app.R.drawable.app_icon),
provider = ImageProvider(R.drawable.app_icon),
contentDescription = null,
modifier = GlanceModifier.size(32.dp),
)

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.widget
package org.meshtastic.feature.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.widget
package org.meshtastic.feature.widget
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers

View file

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.widget
package org.meshtastic.feature.widget
import android.content.Context
import androidx.glance.GlanceId

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.widget.di
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
@Module
@ComponentScan("org.meshtastic.feature.widget")
class FeatureWidgetModule

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2025 Meshtastic LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<group android:scaleX="0.24"
android:scaleY="0.24"
android:translateY="5.4">
<path
android:pathData="M64.716,13.073L37.867,52.447L32.204,48.585L61.878,5.068C62.516,4.132 63.575,3.572 64.707,3.571C65.839,3.57 66.899,4.128 67.538,5.063L97.281,48.512L91.625,52.384L64.716,13.073Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M8.379,52.406L39.741,6.415L34.078,2.553L2.716,48.544L8.379,52.406Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -0,0 +1,27 @@
<!--
~ Copyright (c) 2025 Meshtastic LLC
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View file

@ -4,7 +4,7 @@ This module provides an example implementation of an app that uses the [AIDL](ht
## Overview
The [AIDL](../core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl) is defined in the main app module and is used to interact with the mesh network.
The [AIDL](../core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl) is defined in the `core:api` module and is used to interact with the mesh network.
`mesh_service_example` demonstrates how to build and integrate a custom mesh service within the Meshtastic ecosystem. It is intended as a reference for developers who want to extend or customize mesh-related functionality.

View file

@ -44,6 +44,7 @@ include(
":feature:node",
":feature:settings",
":feature:firmware",
":feature:widget",
":mesh_service_example",
":desktop",
)