fix: fix animation stalls and update dependencies for stability (#4784)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-13 18:01:17 -05:00 committed by GitHub
parent 90844301e8
commit 427c0f3bbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 384 additions and 243 deletions

View file

@ -74,6 +74,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries.
- **Dependencies:** Check `gradle/libs.versions.toml` before 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`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
@ -108,7 +109,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
**Testing:**
```bash
./gradlew test # Run local unit tests
./gradlew testDebugUnitTest # CI-aligned Android unit tests
./gradlew testFdroidDebugUnitTest testGoogleDebugUnitTest # CI-aligned Android unit tests (flavor-explicit)
./gradlew connectedAndroidTest # Run instrumented tests
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks

View file

@ -16,9 +16,9 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **Core Architecture:** Modern Android Development (MAD) with KMP core.
- **KMP Modules:** Most `core:*` modules. All declare `jvm()` target and compile clean on JVM.
- **Android-only Modules:** `core:api` (AIDL), `core:barcode` (CameraX + flavor-specific decoder). Shared contracts abstracted into `core:ui/commonMain`.
- **UI:** Jetpack Compose (Material 3).
- **DI:** Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in `app`.
- **Navigation:** AndroidX Navigation 3 (JetBrains multiplatform fork) with shared backstack state.
- **UI:** Jetpack Compose Multiplatform (Material 3).
- **DI:** Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in `app` and `desktop`.
- **Navigation:** JetBrains Navigation 3 (Multiplatform fork) with shared backstack state.
- **Lifecycle:** JetBrains multiplatform `lifecycle-viewmodel-compose` and `lifecycle-runtime-compose`.
- **Database:** Room KMP.
@ -74,6 +74,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries.
- **Dependencies:** Check `gradle/libs.versions.toml` before 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`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
@ -108,7 +110,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
**Testing:**
```bash
./gradlew test # Run local unit tests
./gradlew testDebugUnitTest # CI-aligned Android unit tests
./gradlew testFdroidDebugUnitTest testGoogleDebugUnitTest # CI-aligned Android unit tests (flavor-explicit)
./gradlew connectedAndroidTest # Run instrumented tests
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks

View file

@ -16,9 +16,9 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **Core Architecture:** Modern Android Development (MAD) with KMP core.
- **KMP Modules:** Most `core:*` modules. All declare `jvm()` target and compile clean on JVM.
- **Android-only Modules:** `core:api` (AIDL), `core:barcode` (CameraX + flavor-specific decoder). Shared contracts abstracted into `core:ui/commonMain`.
- **UI:** Jetpack Compose (Material 3).
- **DI:** Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in `app`.
- **Navigation:** AndroidX Navigation 3 (JetBrains multiplatform fork) with shared backstack state.
- **UI:** Jetpack Compose Multiplatform (Material 3).
- **DI:** Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in `app` and `desktop`.
- **Navigation:** JetBrains Navigation 3 (Multiplatform fork) with shared backstack state.
- **Lifecycle:** JetBrains multiplatform `lifecycle-viewmodel-compose` and `lifecycle-runtime-compose`.
- **Database:** Room KMP.
@ -74,6 +74,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries.
- **Dependencies:** Check `gradle/libs.versions.toml` before 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`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
@ -108,7 +110,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
**Testing:**
```bash
./gradlew test # Run local unit tests
./gradlew testDebugUnitTest # CI-aligned Android unit tests
./gradlew testFdroidDebugUnitTest testGoogleDebugUnitTest # CI-aligned Android unit tests (flavor-explicit)
./gradlew connectedAndroidTest # Run instrumented tests
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks

View file

@ -11,7 +11,7 @@
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
This is a tool for using Android with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware).
This is a tool for using Android (and Compose Desktop) with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware).
This project is currently beta testing across various providers. If you have questions or feedback please [Join our discussion forum](https://github.com/orgs/meshtastic/discussions) or the [Discord Group](https://discord.gg/meshtastic) . We would love to hear from you!
@ -60,11 +60,11 @@ You can generate the documentation locally to preview your changes.
### Modern Android Development (MAD)
The app follows modern Android development practices, built on top of a shared Kotlin Multiplatform (KMP) Core:
- **KMP Modules:** Business logic (`core:domain`), data sources (`core:data`, `core:database`, `core:datastore`), and communications (`core:network`, `core:ble`) are entirely platform-agnostic, enabling future support for Desktop and Web.
- **UI:** Jetpack Compose (Material 3) using Compose Multiplatform resources.
- **KMP Modules:** Business logic (`core:domain`), data sources (`core:data`, `core:database`, `core:datastore`), and communications (`core:network`, `core:ble`) are entirely platform-agnostic, targeting Android and Compose Desktop.
- **UI:** JetBrains Compose Multiplatform (Material 3) using Compose Multiplatform resources.
- **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow.
- **Dependency Injection:** Koin with Koin Annotations (Compiler Plugin).
- **Navigation:** Type-Safe Navigation (Jetpack Navigation).
- **Dependency Injection:** Koin with Koin Annotations (K2 Compiler Plugin).
- **Navigation:** JetBrains Navigation 3 (Multiplatform routing).
- **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms).
### Bluetooth Low Energy (BLE)

View file

@ -235,9 +235,9 @@ dependencies {
implementation(projects.feature.settings)
implementation(projects.feature.firmware)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout)
implementation(libs.androidx.compose.material3.adaptive.navigation)
implementation(libs.jetbrains.compose.material3.adaptive)
implementation(libs.jetbrains.compose.material3.adaptive.layout)
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
implementation(libs.androidx.compose.material3.navigationSuite)
implementation(libs.material)
implementation(libs.androidx.compose.material3)
@ -248,10 +248,10 @@ dependencies {
implementation(libs.androidx.glance.appwidget.preview)
implementation(libs.androidx.glance.material3)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.androidx.paging.compose)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.content.negotiation)

View file

@ -39,7 +39,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.rounded.TripOrigin
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
@ -140,12 +139,7 @@ private const val TRACEROUTE_OFFSET_METERS = 100.0
private const val TRACEROUTE_BOUNDS_PADDING_PX = 120
@Suppress("CyclomaticComplexMethod", "LongMethod")
@OptIn(
MapsComposeExperimentalApi::class,
ExperimentalMaterial3Api::class,
ExperimentalMaterial3ExpressiveApi::class,
ExperimentalPermissionsApi::class,
)
@OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable
fun MapView(
modifier: Modifier = Modifier,
@ -803,7 +797,6 @@ fun Uri.getFileName(context: android.content.Context): String {
return name
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Suppress("LongMethod")
private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayUnits = DisplayUnits.METRIC) {
@ -812,7 +805,7 @@ private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayU
Row(modifier = Modifier.padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) {
Text(label, style = MaterialTheme.typography.labelMedium)
Spacer(modifier = Modifier.width(16.dp))
Text(value, style = MaterialTheme.typography.labelMediumEmphasized)
Text(value, style = MaterialTheme.typography.labelMedium)
}
}

View file

@ -17,6 +17,7 @@
package org.meshtastic.app.map.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
@ -29,8 +30,6 @@ import androidx.compose.material.icons.outlined.Navigation
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material.icons.rounded.LocationDisabled
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalFloatingToolbar
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -47,7 +46,6 @@ import org.meshtastic.core.resources.refresh
import org.meshtastic.core.resources.toggle_my_position
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MapControlsOverlay(
modifier: Modifier = Modifier,
@ -71,86 +69,80 @@ fun MapControlsOverlay(
isRefreshing: Boolean = false,
onRefresh: () -> Unit = {},
) {
HorizontalFloatingToolbar(
modifier = modifier,
expanded = true,
leadingContent = {},
trailingContent = {},
content = {
CompassButton(onClick = onCompassClick, bearing = bearing, isFollowing = followPhoneBearing)
if (isNodeMap) {
Row(modifier = modifier) {
CompassButton(onClick = onCompassClick, bearing = bearing, isFollowing = followPhoneBearing)
if (isNodeMap) {
MapButton(
icon = Icons.Outlined.Tune,
contentDescription = stringResource(Res.string.map_filter),
onClick = onToggleMapFilterMenu,
)
NodeMapFilterDropdown(
expanded = mapFilterMenuExpanded,
onDismissRequest = onMapFilterMenuDismissRequest,
mapViewModel = mapViewModel,
)
} else {
Box {
MapButton(
icon = Icons.Outlined.Tune,
contentDescription = stringResource(Res.string.map_filter),
onClick = onToggleMapFilterMenu,
)
NodeMapFilterDropdown(
MapFilterDropdown(
expanded = mapFilterMenuExpanded,
onDismissRequest = onMapFilterMenuDismissRequest,
mapViewModel = mapViewModel,
)
}
}
Box {
MapButton(
icon = Icons.Outlined.Map,
contentDescription = stringResource(Res.string.map_tile_source),
onClick = onToggleMapTypeMenu,
)
MapTypeDropdown(
expanded = mapTypeMenuExpanded,
onDismissRequest = onMapTypeMenuDismissRequest,
mapViewModel = mapViewModel, // Pass mapViewModel
onManageCustomTileProvidersClicked = onManageCustomTileProvidersClicked, // Pass new callback
)
}
MapButton(
icon = Icons.Outlined.Layers,
contentDescription = stringResource(Res.string.manage_map_layers),
onClick = onManageLayersClicked,
)
if (showRefresh) {
if (isRefreshing) {
Box(modifier = Modifier.padding(8.dp)) {
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
}
} else {
Box {
MapButton(
icon = Icons.Outlined.Tune,
contentDescription = stringResource(Res.string.map_filter),
onClick = onToggleMapFilterMenu,
)
MapFilterDropdown(
expanded = mapFilterMenuExpanded,
onDismissRequest = onMapFilterMenuDismissRequest,
mapViewModel = mapViewModel,
)
}
}
Box {
MapButton(
icon = Icons.Outlined.Map,
contentDescription = stringResource(Res.string.map_tile_source),
onClick = onToggleMapTypeMenu,
)
MapTypeDropdown(
expanded = mapTypeMenuExpanded,
onDismissRequest = onMapTypeMenuDismissRequest,
mapViewModel = mapViewModel, // Pass mapViewModel
onManageCustomTileProvidersClicked = onManageCustomTileProvidersClicked, // Pass new callback
icon = Icons.Filled.Refresh,
contentDescription = stringResource(Res.string.refresh),
onClick = onRefresh,
)
}
}
MapButton(
icon = Icons.Outlined.Layers,
contentDescription = stringResource(Res.string.manage_map_layers),
onClick = onManageLayersClicked,
)
if (showRefresh) {
if (isRefreshing) {
Box(modifier = Modifier.padding(8.dp)) {
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
}
} else {
MapButton(
icon = Icons.Filled.Refresh,
contentDescription = stringResource(Res.string.refresh),
onClick = onRefresh,
)
}
}
// Location tracking button
MapButton(
icon =
if (isLocationTrackingEnabled) {
Icons.Rounded.LocationDisabled
} else {
Icons.Outlined.MyLocation
},
contentDescription = stringResource(Res.string.toggle_my_position),
onClick = onToggleLocationTracking,
)
},
)
// Location tracking button
MapButton(
icon =
if (isLocationTrackingEnabled) {
Icons.Rounded.LocationDisabled
} else {
Icons.Outlined.MyLocation
},
contentDescription = stringResource(Res.string.toggle_my_position),
onClick = onToggleLocationTracking,
)
}
}
@Composable

View file

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

View file

@ -0,0 +1,8 @@
{
"track_id": "fix_android_animations_20260313",
"type": "bug",
"status": "new",
"created_at": "2026-03-13T12:00:00Z",
"updated_at": "2026-03-13T12:00:00Z",
"description": "Android animations broken - mainly noticeable on Connections screen, the indescriminate circular and linear progress bars don't move, and the MeshActivity animation is not firing, investigate recomposition and threading strangely enough they're working on Desktop"
}

View file

@ -0,0 +1,27 @@
# Implementation Plan: Fix Android Animation Stalls
## Phase 1: Research and Reproduction
- [x] Task: Historical Regression Analysis
- [x] Compare current code with pre-2.7.14-internal versions to identify changes in threading or UI state management.
- [x] Check `gh` history for commits related to `ConnectionsScreen` and `MeshActivity` transitions.
- [x] Task: Reproduction and Diagnosis
- [x] Create a reproduction case (manual or automated) that consistently shows stalled progress bars on Android.
- [x] Inspect Recomposition counts using Layout Inspector or logging.
- [x] Verify Coroutine Dispatchers used for UI state updates.
- [x] Task: Conductor - User Manual Verification 'Phase 1: Research and Reproduction' (Protocol in workflow.md)
## Phase 2: Fix Implementation
- [x] Task: Core Animation Fix
- [x] Apply fix to resolve threading/recomposition stalls (e.g., correct `Dispatcher.Main` usage or state hoisting).
- [x] Verify progress bars on Connections screen are animating.
- [x] Task: MeshActivity Transition Fix
- [x] Fix animation firing for `MeshActivity` entries and exits.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Fix Implementation' (Protocol in workflow.md)
## Phase 3: Project-wide Audit and Final Verification
- [x] Task: Audit App Animations
- [x] Scan other screens for similar animation stalls and apply fixes where necessary.
- [x] Task: Automated Testing
- [x] Write/Update Compose UI tests to ensure animations are running on Android.
- [x] Verify no regressions on Desktop.
- [x] Task: Conductor - User Manual Verification 'Phase 3: Project-wide Audit and Final Verification' (Protocol in workflow.md)

View file

@ -0,0 +1,25 @@
# Track Specification: Fix Android Animation Stalls (Regression)
## Overview
This track aims to diagnose and resolve a regression introduced in recent `2.7.14-internal` releases where animations (standard Compose progress indicators and custom transitions) fail to fire on Android. While these animations work correctly on Desktop, they are "stuck" or "stalled" on Android, likely due to threading issues or recomposition failures.
## Historical Context
- **Introduction**: This issue appeared during the `2.7.14-internal` release cycle.
- **Comparison**: Older versions or the current Desktop build can be used as references to identify code changes that might have triggered the regression.
## Functional Requirements
- **Animation Restoration**: Restore movement to indeterminate circular and linear progress bars, particularly on the Connections screen.
- **Transition Fixes**: Ensure `MeshActivity` animations (entry/exit/transitions) fire as expected.
- **Project-wide Audit**: Audit other screens for similar "stuck" animations.
- **KMP Parity**: Ensure shared `commonMain` code functions correctly on both Android and Desktop.
## Non-Functional Requirements
- **Performance**: Ensure no UI jank or excessive recompositions.
- **Verification**: Use historical code comparison (via `gh` or temporary copies) to isolate the breaking change.
## Acceptance Criteria
- [ ] Indeterminate progress bars on the Connections screen animate continuously.
- [ ] `MeshActivity` animations fire correctly.
- [ ] Root cause identified (Regression since 2.7.14-internal).
- [ ] Automated UI tests verify animation behavior on Android.
- [ ] Unit tests verify state flow if threading/ViewModels are involved.

View file

@ -43,7 +43,7 @@ kotlin {
implementation(projects.core.prefs)
implementation(projects.core.proto)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.jetbrains.lifecycle.runtime)
implementation(libs.androidx.paging.common)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kermit)

View file

@ -30,7 +30,7 @@ kotlin {
commonMain.dependencies {
implementation(projects.core.resources)
implementation(libs.kotlinx.serialization.core)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.jetbrains.navigation3.runtime)
}
commonTest.dependencies { implementation(kotlin("test")) }

View file

@ -34,8 +34,8 @@ kotlin {
androidMain.dependencies {
implementation(libs.androidx.activity.compose)
implementation(compose.runtime)
implementation(compose.ui)
implementation(libs.compose.multiplatform.runtime)
implementation(libs.compose.multiplatform.ui)
}
commonTest.dependencies { implementation(kotlin("test")) }

View file

@ -401,6 +401,10 @@
<string name="battery">Battery</string>
<string name="channel_utilization">ChUtil</string>
<string name="air_utilization">AirUtil</string>
<string name="device_metrics_percent_value">%1$s: %2$.1f%%</string>
<string name="device_metrics_voltage_value">%1$s: %2$.1f V</string>
<string name="device_metrics_numeric_value">%1$.1f</string>
<string name="device_metrics_label_value">%1$s: %2$s</string>
<string name="temperature">Temp</string>
<string name="humidity">Hum</string>
<string name="soil_temperature">Soil Temp</string>

View file

@ -44,13 +44,13 @@ kotlin {
implementation(projects.core.resources)
implementation(projects.core.service)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(compose.ui)
implementation(compose.foundation)
implementation(compose.runtime)
implementation(compose.components.resources)
implementation(compose.uiTooling)
implementation(libs.compose.multiplatform.material3)
implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.compose.multiplatform.ui)
implementation(libs.compose.multiplatform.foundation)
implementation(libs.compose.multiplatform.runtime)
implementation(libs.compose.multiplatform.resources)
implementation(libs.compose.multiplatform.ui.tooling)
implementation(libs.kermit)
implementation(libs.koin.compose.viewmodel)

View file

@ -107,11 +107,11 @@ dependencies {
// Compose Desktop
implementation(compose.desktop.currentOs)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.components.resources)
implementation(libs.compose.multiplatform.material3)
implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.compose.multiplatform.runtime)
implementation(libs.compose.multiplatform.foundation)
implementation(libs.compose.multiplatform.resources)
// JetBrains Material 3 Adaptive (multiplatform ListDetailPaneScaffold)
implementation(libs.jetbrains.compose.material3.adaptive)
@ -119,10 +119,10 @@ dependencies {
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
// Navigation 3 (JetBrains fork — multiplatform)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
implementation(libs.jetbrains.lifecycle.runtime.compose)
// Koin DI
implementation(libs.koin.core)

View file

@ -105,6 +105,7 @@ Build Verification:
- **Removed:** Manual `dependsOn(...)` wiring from `core:common`, `core:model`, `core:network`, and `core:ui`
- **Analyzed:** Composition opportunities for other duplicate plugins
- **Documented:** Future optimization paths and consolidation criteria
- **Migrated:** JetBrains Compose Multiplatform dependencies from hard-coded/legacy `compose.xyz` references to proper version catalog entries.
---
@ -136,6 +137,7 @@ Build Verification:
### Short Term
- [ ] Consider plugin validation test suite
- [ ] Review other configuration functions for consolidation opportunities
- [ ] Investigate factoring out JetBrains CMP dependencies into `meshtastic.kmp.library.compose` convention.
### Long Term
- [ ] Monitor if Android Application/Library handling diverges

View file

@ -9,16 +9,33 @@ Use `AGENTS.md` as the source of truth for architecture boundaries and required
When checking upstream docs/examples, match these repository-pinned versions from `gradle/libs.versions.toml`:
- Kotlin: `2.3.10`
- Koin: `4.2.0-RC1` (`koin-annotations` `2.1.0`, compiler plugin `0.3.0`)
- AndroidX Navigation 3 (JetBrains fork): `1.1.0-alpha03` (`org.jetbrains.androidx.navigation3`)
- JetBrains Lifecycle (multiplatform): `2.10.0-alpha08` (`org.jetbrains.androidx.lifecycle`)
- AndroidX Lifecycle (Android-only): `2.10.0`
- Koin: `4.2.0-RC2` (`koin-annotations` `2.1.0`, compiler plugin `0.4.0`)
- JetBrains Navigation 3: `1.1.0-alpha04` (`org.jetbrains.androidx.navigation3`)
- JetBrains Lifecycle (multiplatform): `2.10.0-beta01` (`org.jetbrains.androidx.lifecycle`)
- AndroidX Lifecycle (Android-only): `2.10.0` (`androidx.lifecycle`)
- Kotlin Coroutines: `1.10.2`
- Compose Multiplatform: `1.11.0-alpha03`
- JetBrains Material 3 Adaptive: `1.3.0-alpha05` (`org.jetbrains.compose.material3.adaptive`)
- Compose Multiplatform: `1.11.0-alpha04`
- JetBrains Material 3 Adaptive: `1.3.0-alpha06` (`org.jetbrains.compose.material3.adaptive`)
Prefer versioned docs pages that match those versions (for example, Koin `4.2` docs rather than older `4.0/4.1` pages).
## Dependency alias quick-reference
Version catalog aliases split cleanly by fork provenance. **Use the right prefix for the right source set.**
| Alias prefix | Coordinates | Use in |
|---|---|---|
| `jetbrains-lifecycle-*` | `org.jetbrains.androidx.lifecycle:*` | `commonMain`, `androidMain` |
| `jetbrains-navigation3-*` | `org.jetbrains.androidx.navigation3:*` | `commonMain`, `androidMain` |
| `jetbrains-compose-material3-adaptive-*` | `org.jetbrains.compose.material3.adaptive:*` | `commonMain`, `androidMain` |
| `androidx-lifecycle-process` | `androidx.lifecycle:lifecycle-process` | `androidMain` only — `ProcessLifecycleOwner` |
| `androidx-lifecycle-runtime-ktx` | `androidx.lifecycle:lifecycle-runtime-ktx` | `androidMain` only |
| `androidx-lifecycle-viewmodel-ktx` | `androidx.lifecycle:lifecycle-viewmodel-ktx` | `androidMain` only |
| `androidx-lifecycle-testing` | `androidx.lifecycle:lifecycle-runtime-testing` | `androidUnitTest` only |
| `androidx-navigation-common` | `androidx.navigation:navigation-common` | `androidMain` only |
> `jetbrains-navigation3-runtime` and `jetbrains-navigation3-ui` resolve to the same `navigation3-ui` artifact — JetBrains does not publish a separate runtime artifact yet.
Quick references:
- Koin annotations (4.2 docs): `https://insert-koin.io/docs/reference/koin-annotations/start`
@ -37,5 +54,3 @@ Quick references:

View file

@ -35,6 +35,34 @@ Both modules still define separate graph-builder files (`app/navigation/*.kt`, `
4. **Route keys are shared; graph registration is per-platform.**
- This is the expected state — platform shells wire entries differently while consuming the same route types.
## Alpha04 Changelog Impact Check (2026-03-13)
Source reviewed: Compose Multiplatform `v1.11.0-alpha04` release notes.
1. **No direct Navigation 3 API breakage called out.**
- Release notes include component version bumps for Navigation 3 (`1.1.0-alpha04`) but no `NavBackStack`, `NavDisplay`, or `entryProvider` API migration requirements.
- Existing shell patterns in `app` and `desktop` remain valid.
2. **Primary risk is dependency wiring drift, not runtime behavior.**
- JetBrains Navigation 3 currently publishes `navigation3-ui` coordinates (no separate `navigation3-runtime` artifact in Maven Central). The `jetbrains-navigation3-runtime` alias intentionally points to `navigation3-ui` and is documented in the version catalog.
3. **Saved-state and typed-route parity risk remains unchanged.**
- Desktop still uses manual serializer registration; this is an existing risk and not introduced by alpha04.
4. **Compose-wide migration notes do not currently impact navigation codepaths.**
- `Shader` wrapper changes and `Canvas.nativeCanvas` deprecations are not used in the Navigation 3 shell files.
### Actions Taken
- Renamed all JetBrains-forked lifecycle/nav3 version catalog aliases from `androidx-*` to `jetbrains-*` prefix to make fork provenance unambiguous:
- `jetbrains-lifecycle-runtime`, `jetbrains-lifecycle-runtime-compose`, `jetbrains-lifecycle-viewmodel-compose`, `jetbrains-lifecycle-viewmodel-navigation3`
- `jetbrains-navigation3-runtime`, `jetbrains-navigation3-ui`
- Documented in the version catalog that `jetbrains-navigation3-runtime` intentionally maps to `navigation3-ui` until a separate runtime artifact is published.
- Migrated `core:data` `commonMain` from `androidx.lifecycle:lifecycle-runtime` (Google) to `org.jetbrains.androidx.lifecycle:lifecycle-runtime` (JetBrains fork) for full consistency.
- Updated active docs to reflect the current dependency baseline (`1.11.0-alpha04`, `1.1.0-alpha04`, `1.3.0-alpha06`, `2.10.0-beta01`).
- Consolidated `app` adaptive dependencies to JetBrains Material 3 Adaptive coordinates (`org.jetbrains.compose.material3.adaptive:*`) so Android and Desktop consume the same adaptive artifact family. The Android-only navigation suite remains on `androidx.compose.material3:material3-adaptive-navigation-suite`.
### Deferred Follow-ups
- Add automated validation that desktop serializer registrations stay in sync with shared route keys.
## Options Evaluated
### Option A: Reuse `:app` navigation implementation directly in desktop

View file

@ -1,6 +1,6 @@
# KMP Migration Status
> Last updated: 2026-03-12
> Last updated: 2026-03-13
Single source of truth for Kotlin Multiplatform migration progress. For the forward-looking roadmap, see [`roadmap.md`](./roadmap.md). For completed decision records, see [`decisions/`](./decisions/).
@ -105,7 +105,8 @@ Based on the latest codebase investigation, the following steps are proposed to
| Navigation 3 parity model (shared `TopLevelDestination` + platform adapters) | ✅ Done | Both shells use shared enum + parity tests. See [`decisions/navigation3-parity-2026-03.md`](./decisions/navigation3-parity-2026-03.md) |
| Hilt → Koin | ✅ Done | See [`decisions/koin-migration.md`](./decisions/koin-migration.md) |
| BLE abstraction (Nordic Hybrid) | ✅ Done | See [`decisions/ble-strategy.md`](./decisions/ble-strategy.md) |
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha05` aligned with CMP `1.11.0-alpha03` |
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-alpha04` |
| JetBrains lifecycle/nav3 alias alignment | ✅ Done | All forked deps use `jetbrains-*` prefix in version catalog; `core:data` commonMain uses JetBrains lifecycle runtime |
| Expect/actual consolidation | ✅ Done | 7 pairs eliminated; 15+ genuinely platform-specific retained |
| Transport deduplication | ✅ Done | `StreamFrameCodec` + `TcpTransport` shared in `core:network` |
| **Transport UI Unification** | ✅ Done | `RadioInterfaceService` provides dynamic transport capability to shared UI |
@ -140,10 +141,10 @@ Extracted to shared `commonMain` (no longer app-only):
| Dependency | Version | Why |
|---|---|---|
| Compose Multiplatform | `1.11.0-alpha03` | Required for JetBrains Adaptive `1.3.0-alpha05` |
| Koin | `4.2.0-RC1` | Nav3 + K2 compiler plugin support |
| JetBrains Lifecycle | `2.10.0-alpha08` | Multiplatform ViewModel/lifecycle |
| JetBrains Navigation 3 | `1.1.0-alpha03` | Multiplatform navigation |
| Compose Multiplatform | `1.11.0-alpha04` | Required for JetBrains Adaptive `1.3.0-alpha06` |
| Koin | `4.2.0-RC2` | Nav3 + K2 compiler plugin support |
| JetBrains Lifecycle | `2.10.0-beta01` | Multiplatform ViewModel/lifecycle |
| JetBrains Navigation 3 | `1.1.0-alpha04` | Multiplatform navigation |
| Nordic BLE | `2.0.0-alpha16` | Behind abstraction boundary |
**Policy:** Stable by default. RC when it unlocks KMP functionality. Alpha only behind hard abstraction seams. Do not downgrade CMP or Koin — they enable critical KMP features.

View file

@ -16,6 +16,7 @@ These items address structural gaps identified in the March 2026 architecture re
| Add feature module `commonTest` (settings, node, messaging) | Medium | Medium | ✅ |
| Desktop Koin `checkModules()` integration test | Medium | Low | ✅ |
| Auto-wire Desktop ViewModels via K2 Compiler (eliminate manual wiring) | Medium | Low | ✅ |
| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | ✅ |
## Active Work
@ -80,6 +81,7 @@ These items address structural gaps identified in the March 2026 architecture re
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****Planned:** Consolidate expansive build-logic convention plugins. There is currently some duplication in Compose dependencies that should be factored into common conventions (`meshtastic.kmp.library.compose` vs manually specifying JetBrains CMP deps in feature modules).
## Medium-Term Priorities (60 days)

View file

@ -33,9 +33,9 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(compose.foundation)
implementation(libs.compose.multiplatform.material3)
implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.compose.multiplatform.foundation)
implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.database)
@ -52,8 +52,8 @@ kotlin {
implementation(projects.core.ble)
implementation(projects.feature.settings)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.koin.compose.viewmodel)
implementation(libs.kermit)
}
@ -66,7 +66,7 @@ kotlin {
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.ui.text)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.usb.serial.android)
}

View file

@ -23,7 +23,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -77,8 +79,11 @@ open class ScannerViewModel(
timeout = kotlin.time.Duration.INFINITE,
serviceUuid = org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID,
)
.flowOn(kotlinx.coroutines.Dispatchers.IO)
.collect { device ->
scannedBleDevices.update { current -> current + (device.address to device) }
if (!scannedBleDevices.value.containsKey(device.address)) {
scannedBleDevices.update { current -> current + (device.address to device) }
}
}
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
co.touchlab.kermit.Logger.w(e) { "BLE scan failed" }
@ -113,22 +118,29 @@ open class ScannerViewModel(
// Sort by name
(bonded + unbondedScanned).sortedBy { it.name }
}
.flowOn(kotlinx.coroutines.Dispatchers.Default)
.distinctUntilChanged()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
/** UI StateFlow for USB devices. */
val usbDevicesForUi: StateFlow<List<DeviceListEntry>> =
discoveredDevicesFlow.map { it?.usbDevices ?: emptyList() }.stateInWhileSubscribed(initialValue = emptyList())
discoveredDevicesFlow
.map { it?.usbDevices ?: emptyList() }
.distinctUntilChanged()
.stateInWhileSubscribed(initialValue = emptyList())
/** UI StateFlow for discovered TCP devices. */
/** UI StateFlow for discovered TCP devices (NSD). */
val discoveredTcpDevicesForUi: StateFlow<List<DeviceListEntry>> =
discoveredDevicesFlow
.map { it?.discoveredTcpDevices ?: emptyList() }
.distinctUntilChanged()
.stateInWhileSubscribed(initialValue = emptyList())
/** UI StateFlow for recently connected TCP devices that are not currently discovered. */
/** UI StateFlow for recent TCP devices. */
val recentTcpDevicesForUi: StateFlow<List<DeviceListEntry>> =
discoveredDevicesFlow
.map { it?.recentTcpDevices ?: emptyList() }
.distinctUntilChanged()
.stateInWhileSubscribed(initialValue = emptyList())
val selectedAddressFlow: StateFlow<String?> = radioInterfaceService.currentDeviceAddressFlow

View file

@ -47,6 +47,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.distinctUntilChanged
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@ -104,7 +105,20 @@ fun ConnectionsScreen(
val config by connectionsViewModel.localConfig.collectAsStateWithLifecycle()
val scanStatusText by scanModel.errorText.collectAsStateWithLifecycle()
val connectionState by connectionsViewModel.connectionState.collectAsStateWithLifecycle()
val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
// Prevent continuous recomposition from lastHeard and snr updates on the node
val ourNode by
remember(connectionsViewModel.ourNodeInfo) {
connectionsViewModel.ourNodeInfo.distinctUntilChanged { old, new ->
old?.num == new?.num &&
old?.user == new?.user &&
old?.batteryLevel == new?.batteryLevel &&
old?.voltage == new?.voltage &&
old?.metadata?.firmware_version == new?.metadata?.firmware_version
}
}
.collectAsStateWithLifecycle(initialValue = connectionsViewModel.ourNodeInfo.value)
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
val regionUnset = config.lora?.region == Config.LoRaConfig.RegionCode.UNSET

View file

@ -26,7 +26,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
@ -35,8 +34,7 @@ import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.conflate
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.MeshActivity
@ -56,13 +54,12 @@ fun AnimatedConnectionsNavIcon(
) {
var currentGlowColor by remember { mutableStateOf(Color.Transparent) }
val animatedGlowAlpha = remember { Animatable(0f) }
val coroutineScope = rememberCoroutineScope()
val sendColor = colorScheme.StatusGreen
val receiveColor = colorScheme.StatusBlue
LaunchedEffect(meshActivityFlow, colorScheme) {
meshActivityFlow.collectLatest { activity ->
meshActivityFlow.conflate().collect { activity ->
val newTargetColor =
when (activity) {
is MeshActivity.Send -> sendColor
@ -70,15 +67,15 @@ fun AnimatedConnectionsNavIcon(
}
currentGlowColor = newTargetColor
// Launching in a new coroutine ensures the collect block is not suspended.
coroutineScope.launch {
animatedGlowAlpha.stop()
animatedGlowAlpha.snapTo(1.0f)
animatedGlowAlpha.animateTo(
targetValue = 0.0f,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
)
}
// Suspend the collection until the animation finishes.
// conflate() will drop any fast events that arrive during this 1-second animation.
animatedGlowAlpha.stop()
animatedGlowAlpha.snapTo(1.0f)
animatedGlowAlpha.animateTo(
targetValue = 0.0f,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
)
}
}

View file

@ -48,7 +48,7 @@ kotlin {
implementation(projects.core.resources)
implementation(projects.core.ui)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.kermit)
implementation(libs.kotlinx.collections.immutable)

View file

@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("TooManyFunctions")
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
package org.meshtastic.feature.firmware
@ -46,13 +45,12 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
@ -228,6 +226,7 @@ fun FirmwareUpdateScreen(onNavigateUp: () -> Unit, viewModel: FirmwareUpdateView
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun FirmwareUpdateScaffold(
onNavigateUp: () -> Unit,
@ -342,7 +341,7 @@ private fun FirmwareUpdateContent(
@Composable
private fun VerifyingState() {
CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
CircularProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(stringResource(Res.string.firmware_update_verifying), style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
@ -357,7 +356,7 @@ private fun VerifyingState() {
@Composable
private fun CheckingState() {
CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
CircularProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(stringResource(Res.string.firmware_update_checking), style = MaterialTheme.typography.bodyLarge)
}
@ -706,7 +705,7 @@ private fun ProgressContent(
tint = MaterialTheme.colorScheme.primary,
)
} else {
CircularWavyProgressIndicator(
CircularProgressIndicator(
progress = { if (isUpdating) progressState.progress else 1f },
modifier = Modifier.size(64.dp),
)
@ -730,7 +729,7 @@ private fun ProgressContent(
Spacer(Modifier.height(12.dp))
if (isDownloading || isUpdating) {
LinearWavyProgressIndicator(
LinearProgressIndicator(
progress = { progressState.progress },
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
)
@ -761,7 +760,7 @@ private fun AwaitingFileSaveState(state: FirmwareUpdateState.AwaitingFileSave, o
)
}
CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
CircularProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(
stringResource(Res.string.firmware_update_save_dfu_file),

View file

@ -40,9 +40,9 @@ kotlin {
implementation(projects.core.ui)
implementation(projects.core.resources)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.jetbrains.navigation3.runtime)
}
androidMain.dependencies {
@ -53,7 +53,7 @@ kotlin {
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.ui.text)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.navigation3.ui)
implementation(libs.jetbrains.navigation3.ui)
}
commonTest.dependencies { implementation(projects.core.testing) }

View file

@ -46,7 +46,7 @@ kotlin {
implementation(projects.core.ui)
implementation(projects.core.di)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.koin.compose.viewmodel)
}
@ -61,7 +61,7 @@ kotlin {
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.text)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.navigation.common)

View file

@ -33,9 +33,9 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(compose.foundation)
implementation(libs.compose.multiplatform.material3)
implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.compose.multiplatform.foundation)
implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.database)
@ -48,8 +48,8 @@ kotlin {
implementation(projects.core.service)
implementation(projects.core.ui)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.koin.compose.viewmodel)
implementation(libs.kermit)
implementation(libs.androidx.paging.common)
@ -68,7 +68,7 @@ kotlin {
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.ui.text)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.work.runtime.ktx)

View file

@ -34,8 +34,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(libs.compose.multiplatform.material3)
implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.coil)
implementation(projects.core.common)
implementation(projects.core.data)
@ -52,8 +52,9 @@ kotlin {
implementation(projects.core.di)
implementation(projects.feature.map)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.navigation3.runtime)
implementation(libs.koin.compose.viewmodel)
implementation(libs.kermit)
implementation(libs.kotlinx.collections.immutable)

View file

@ -30,10 +30,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@ -42,7 +40,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
@ -68,7 +65,6 @@ import org.meshtastic.feature.node.component.NodeFilterTextField
import org.meshtastic.feature.node.component.NodeItem
import org.meshtastic.proto.SharedContact
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun NodeListScreen(
@ -125,21 +121,18 @@ fun NodeListScreen(
floatingActionButton = {
val shareCapable = ourNode?.capabilities?.supportsQrCodeSharing ?: false
val sharedContact: SharedContact? by viewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
MeshtasticImportFAB(
sharedContact = sharedContact,
modifier =
Modifier.animateFloatingActionButton(
visible = !isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable,
alignment = Alignment.BottomEnd,
),
onImport = { uriString ->
viewModel.handleScannedUri(uriString) {
scope.launch { context.showToast(Res.string.channel_invalid) }
}
},
onDismissSharedContact = { viewModel.setSharedContactRequested(null) },
isContactContext = true,
)
if (!isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable) {
MeshtasticImportFAB(
sharedContact = sharedContact,
onImport = { uriString ->
viewModel.handleScannedUri(uriString) {
scope.launch { context.showToast(Res.string.channel_invalid) }
}
},
onDismissSharedContact = { viewModel.setSharedContactRequested(null) },
isContactContext = true,
)
}
},
) { contentPadding ->
Box(modifier = Modifier.fillMaxSize().padding(contentPadding).focusable()) {

View file

@ -70,7 +70,11 @@ import org.meshtastic.core.resources.air_utilization
import org.meshtastic.core.resources.battery
import org.meshtastic.core.resources.ch_util_definition
import org.meshtastic.core.resources.channel_utilization
import org.meshtastic.core.resources.device_metrics_label_value
import org.meshtastic.core.resources.device_metrics_log
import org.meshtastic.core.resources.device_metrics_numeric_value
import org.meshtastic.core.resources.device_metrics_percent_value
import org.meshtastic.core.resources.device_metrics_voltage_value
import org.meshtastic.core.resources.uptime
import org.meshtastic.core.resources.voltage
import org.meshtastic.core.ui.component.MaterialBatteryInfo
@ -240,16 +244,23 @@ private fun DeviceMetricsChart(
val voltageColor = Device.VOLTAGE.color
val chUtilColor = Device.CH_UTIL.color
val airUtilColor = Device.AIR_UTIL.color
val batteryLabel = stringResource(Res.string.battery)
val voltageLabel = stringResource(Res.string.voltage)
val channelUtilizationLabel = stringResource(Res.string.channel_utilization)
val airUtilizationLabel = stringResource(Res.string.air_utilization)
val percentValueTemplate = stringResource(Res.string.device_metrics_percent_value)
val voltageValueTemplate = stringResource(Res.string.device_metrics_voltage_value)
val numericValueTemplate = stringResource(Res.string.device_metrics_numeric_value)
val marker =
ChartStyling.rememberMarker(
valueFormatter =
ChartStyling.createColoredMarkerValueFormatter { value, color ->
when (color.copy(alpha = 1f)) {
batteryColor -> "Battery: %.1f%%".format(value)
voltageColor -> "Voltage: %.1f V".format(value)
chUtilColor -> "ChUtil: %.1f%%".format(value)
airUtilColor -> "AirUtil: %.1f%%".format(value)
else -> "%.1f".format(value)
batteryColor -> percentValueTemplate.format(batteryLabel, value)
voltageColor -> voltageValueTemplate.format(voltageLabel, value)
chUtilColor -> percentValueTemplate.format(channelUtilizationLabel, value)
airUtilColor -> percentValueTemplate.format(airUtilizationLabel, value)
else -> numericValueTemplate.format(value)
}
},
)
@ -422,6 +433,11 @@ private fun DeviceMetricsChartPreview() {
private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick: () -> Unit) {
val deviceMetrics = telemetry.device_metrics
val time = telemetry.time.toLong() * MS_PER_SEC
val channelUtilizationLabel = stringResource(Res.string.channel_utilization)
val airUtilizationLabel = stringResource(Res.string.air_utilization)
val uptimeLabel = stringResource(Res.string.uptime)
val percentValueTemplate = stringResource(Res.string.device_metrics_percent_value)
val labelValueTemplate = stringResource(Res.string.device_metrics_label_value)
Card(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp).clickable { onClick() },
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
@ -471,7 +487,11 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
MetricIndicator(Device.CH_UTIL.color)
Spacer(Modifier.width(4.dp))
Text(
text = "Ch: %.1f%%".format(deviceMetrics.channel_utilization ?: 0f),
text =
percentValueTemplate.format(
channelUtilizationLabel,
deviceMetrics.channel_utilization ?: 0f,
),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
@ -481,7 +501,11 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
MetricIndicator(Device.AIR_UTIL.color)
Spacer(Modifier.width(4.dp))
Text(
text = "Air: %.1f%%".format(deviceMetrics.air_util_tx ?: 0f),
text =
percentValueTemplate.format(
airUtilizationLabel,
deviceMetrics.air_util_tx ?: 0f,
),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
@ -489,9 +513,10 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
}
Text(
text =
stringResource(Res.string.uptime) +
": " +
labelValueTemplate.format(
uptimeLabel,
formatUptime(deviceMetrics?.uptime_seconds ?: 0),
),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)

View file

@ -33,8 +33,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(libs.compose.multiplatform.material3)
implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.database)
@ -49,8 +49,8 @@ kotlin {
implementation(projects.core.ui)
implementation(projects.core.di)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.kermit)
implementation(libs.kotlinx.collections.immutable)

View file

@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@ -31,10 +30,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.PhoneAndroid
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults.MediumContainerHeight
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -150,7 +147,6 @@ private val Config.DeviceConfig.RebroadcastMode.description: StringResource
Res.string.rebroadcast_mode_core_portnums_only_desc
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Suppress("DEPRECATION", "LongMethod")
@Composable
fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
@ -283,7 +279,7 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
HorizontalDivider()
TextButton(
modifier = Modifier.height(MediumContainerHeight).fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
enabled = state.connected,
shape = RectangleShape,
onClick = { formState.value = formState.value.copy(tzdef = appTzPosixString) },

View file

@ -5,14 +5,13 @@ appcompat = "1.7.1"
accompanist = "0.37.3"
# androidx
androidxComposeMaterial3Adaptive = "1.2.0"
androidxTracing = "1.10.5"
datastore = "1.2.1"
glance = "1.2.0-rc01"
lifecycle = "2.10.0"
jetbrains-lifecycle = "2.10.0-beta01"
navigation = "2.9.7"
navigation3 = "1.1.0-alpha03"
navigation3 = "1.1.0-alpha04"
paging = "3.4.2"
room = "2.8.4"
savedstate = "1.4.0"
@ -81,25 +80,26 @@ androidx-core-location-altitude = { module = "androidx.core:core-location-altitu
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.2.0" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
androidx-emoji2-emojipicker = { module = "androidx.emoji2:emoji2-emojipicker", version = "1.6.0" }
androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
androidx-glance-appwidget-preview = { module = "androidx.glance:glance-appwidget-preview", version.ref = "glance" }
androidx-glance-preview = { module = "androidx.glance:glance-preview", version.ref = "glance" }
androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" }
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
# Android-only lifecycle (no KMP equivalent — use only in androidMain)
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "jetbrains-lifecycle" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-testing = { module = "androidx.lifecycle:lifecycle-runtime-testing", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "jetbrains-lifecycle" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" }
# JetBrains KMP lifecycle (use in commonMain and androidMain)
jetbrains-lifecycle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime", version.ref = "jetbrains-lifecycle" }
jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "jetbrains-lifecycle" }
jetbrains-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "jetbrains-lifecycle" }
jetbrains-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" }
# AndroidX Navigation (legacy nav-compose; Android-only nav utilities)
androidx-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "navigation" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "navigation" }
androidx-navigation3-runtime = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
androidx-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
# JetBrains Navigation 3 currently publishes `navigation3-ui` (no separate `navigation3-runtime` artifact).
# Both `jetbrains-navigation3-runtime` and `jetbrains-navigation3-ui` resolve to the same coordinate.
jetbrains-navigation3-runtime = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
androidx-paging-common = { module = "androidx.paging:paging-common", version.ref = "paging" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
@ -113,15 +113,11 @@ androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version
androidx-work-testing = { module = "androidx.work:work-testing", version = "2.11.1" }
# AndroidX Compose
androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version = "2026.03.00" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version = "2025.12.00" }
androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-material3-navigationSuite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" }
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "androidxComposeMaterial3Adaptive" }
androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" }
androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" }
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
androidx-compose-runtime-tracing = { module = "androidx.compose.runtime:runtime-tracing", version.ref = "androidxTracing" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
@ -132,7 +128,12 @@ androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling
# Compose Multiplatform
compose-multiplatform-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-multiplatform" }
compose-multiplatform-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" }
compose-multiplatform-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-multiplatform" }
compose-multiplatform-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "compose-multiplatform" }
compose-multiplatform-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" }
compose-multiplatform-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "compose-multiplatform" }
compose-multiplatform-materialIconsExtended = { module = "org.jetbrains.compose.material:material-icons-extended", version = "1.7.3" }
# JetBrains Material 3 Adaptive (multiplatform — Android, Desktop, iOS)
jetbrains-compose-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "jetbrains-adaptive" }
@ -143,7 +144,6 @@ jetbrains-compose-material3-adaptive-navigation = { module = "org.jetbrains.comp
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.10.0" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
guava = { module = "com.google.guava:guava", version = "33.5.0-jre" }
location-services = { module = "com.google.android.gms:play-services-location", version = "21.3.0" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
@ -168,7 +168,6 @@ dokka-gradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", versi
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.4.0" }
kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version = "0.31.0" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-android" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines-android" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-android" }
@ -199,7 +198,6 @@ aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-core = { module = "io.coil-kt.coil3:coil-network-core", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
dd-sdk-android-compose = { module = "com.datadoghq:dd-sdk-android-compose", version.ref = "dd-sdk-android" }
@ -224,7 +222,6 @@ nordic-ble-env-android = { module = "no.nordicsemi.kotlin.ble:environment-androi
nordic-ble-env-android-compose = { module = "no.nordicsemi.kotlin.ble:environment-android-compose", version.ref = "nordic-ble" }
nordic-common-core = { module = "no.nordicsemi.android.common:core", version.ref = "nordic-common" }
nordic-common-logger = { module = "no.nordicsemi.android.common:logger", version.ref = "nordic-common" }
nordic-common-permissions-ble = { module = "no.nordicsemi.android.common:permissions-ble", version.ref = "nordic-common" }
nordic-common-permissions-notification = { module = "no.nordicsemi.android.common:permissions-notification", version.ref = "nordic-common" }
nordic-common-scanner-ble = { module = "no.nordicsemi.android.common:scanner-ble", version.ref = "nordic-common" }

View file

@ -42,8 +42,8 @@ dependencies {
implementation(projects.core.proto)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.lifecycle.runtime)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.material)