From 427c0f3bbb22955aced6f80150701bac2084dc85 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:01:17 -0500 Subject: [PATCH] fix: fix animation stalls and update dependencies for stability (#4784) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .github/copilot-instructions.md | 3 +- AGENTS.md | 10 +- GEMINI.md | 10 +- README.md | 10 +- app/build.gradle.kts | 14 +- .../kotlin/org/meshtastic/app/map/MapView.kt | 11 +- .../app/map/component/MapControlsOverlay.kt | 130 ++++++++---------- .../fix_android_animations_20260313/index.md | 5 + .../metadata.json | 8 ++ .../fix_android_animations_20260313/plan.md | 27 ++++ .../fix_android_animations_20260313/spec.md | 25 ++++ core/data/build.gradle.kts | 2 +- core/navigation/build.gradle.kts | 2 +- core/nfc/build.gradle.kts | 4 +- .../composeResources/values/strings.xml | 4 + core/ui/build.gradle.kts | 14 +- desktop/build.gradle.kts | 18 +-- docs/BUILD_LOGIC_INDEX.md | 2 + docs/agent-playbooks/README.md | 31 +++-- docs/decisions/navigation3-parity-2026-03.md | 28 ++++ docs/kmp-status.md | 13 +- docs/roadmap.md | 2 + feature/connections/build.gradle.kts | 12 +- .../feature/connections/ScannerViewModel.kt | 20 ++- .../connections/ui/ConnectionsScreen.kt | 16 ++- .../components/AnimatedConnectionsNavIcon.kt | 25 ++-- feature/firmware/build.gradle.kts | 2 +- .../feature/firmware/FirmwareUpdateScreen.kt | 17 ++- feature/intro/build.gradle.kts | 6 +- feature/map/build.gradle.kts | 4 +- feature/messaging/build.gradle.kts | 12 +- feature/node/build.gradle.kts | 9 +- .../feature/node/list/NodeListScreen.kt | 31 ++--- .../feature/node/metrics/DeviceMetrics.kt | 43 ++++-- feature/settings/build.gradle.kts | 8 +- .../radio/component/DeviceConfigItemList.kt | 6 +- gradle/libs.versions.toml | 39 +++--- mesh_service_example/build.gradle.kts | 4 +- 38 files changed, 384 insertions(+), 243 deletions(-) create mode 100644 conductor/archive/fix_android_animations_20260313/index.md create mode 100644 conductor/archive/fix_android_animations_20260313/metadata.json create mode 100644 conductor/archive/fix_android_animations_20260313/plan.md create mode 100644 conductor/archive/fix_android_animations_20260313/spec.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1e7418801..3810477f6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 1e7418801..01f70faf7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/GEMINI.md b/GEMINI.md index 1e7418801..01f70faf7 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -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 diff --git a/README.md b/README.md index c05a4f17e..17b33a62e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f54d094a3..4808d8b65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt index a67087399..bbda314d9 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -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) } } diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/MapControlsOverlay.kt b/app/src/google/kotlin/org/meshtastic/app/map/component/MapControlsOverlay.kt index e2a73718f..19cb41184 100644 --- a/app/src/google/kotlin/org/meshtastic/app/map/component/MapControlsOverlay.kt +++ b/app/src/google/kotlin/org/meshtastic/app/map/component/MapControlsOverlay.kt @@ -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 diff --git a/conductor/archive/fix_android_animations_20260313/index.md b/conductor/archive/fix_android_animations_20260313/index.md new file mode 100644 index 000000000..35c1f67ac --- /dev/null +++ b/conductor/archive/fix_android_animations_20260313/index.md @@ -0,0 +1,5 @@ +# Track fix_android_animations_20260313 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/archive/fix_android_animations_20260313/metadata.json b/conductor/archive/fix_android_animations_20260313/metadata.json new file mode 100644 index 000000000..6add289e4 --- /dev/null +++ b/conductor/archive/fix_android_animations_20260313/metadata.json @@ -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" +} diff --git a/conductor/archive/fix_android_animations_20260313/plan.md b/conductor/archive/fix_android_animations_20260313/plan.md new file mode 100644 index 000000000..09138e3ee --- /dev/null +++ b/conductor/archive/fix_android_animations_20260313/plan.md @@ -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) diff --git a/conductor/archive/fix_android_animations_20260313/spec.md b/conductor/archive/fix_android_animations_20260313/spec.md new file mode 100644 index 000000000..c8d3cfe63 --- /dev/null +++ b/conductor/archive/fix_android_animations_20260313/spec.md @@ -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. diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index de6ae60a5..6e45f562a 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -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) diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index bdc0135f8..a397ce986 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -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")) } diff --git a/core/nfc/build.gradle.kts b/core/nfc/build.gradle.kts index 2af252501..fe52cea5c 100644 --- a/core/nfc/build.gradle.kts +++ b/core/nfc/build.gradle.kts @@ -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")) } diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index f3410fb0d..82a361465 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -401,6 +401,10 @@ Battery ChUtil AirUtil + %1$s: %2$.1f%% + %1$s: %2$.1f V + %1$.1f + %1$s: %2$s Temp Hum Soil Temp diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index ba3ac6560..8ea749209 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -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) diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 039f5abf1..6934658ef 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -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) diff --git a/docs/BUILD_LOGIC_INDEX.md b/docs/BUILD_LOGIC_INDEX.md index 91dd1f312..20853b83f 100644 --- a/docs/BUILD_LOGIC_INDEX.md +++ b/docs/BUILD_LOGIC_INDEX.md @@ -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 diff --git a/docs/agent-playbooks/README.md b/docs/agent-playbooks/README.md index 904a699e3..a80780f7c 100644 --- a/docs/agent-playbooks/README.md +++ b/docs/agent-playbooks/README.md @@ -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: - - diff --git a/docs/decisions/navigation3-parity-2026-03.md b/docs/decisions/navigation3-parity-2026-03.md index 94a0bf446..2b5596a12 100644 --- a/docs/decisions/navigation3-parity-2026-03.md +++ b/docs/decisions/navigation3-parity-2026-03.md @@ -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 diff --git a/docs/kmp-status.md b/docs/kmp-status.md index 77ef70e20..6d4de8911 100644 --- a/docs/kmp-status.md +++ b/docs/kmp-status.md @@ -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. diff --git a/docs/roadmap.md b/docs/roadmap.md index 45161fa3e..f635cae7e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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) diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index ce94bb390..6b43d6376 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -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) } diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt index 4f2ed0581..2afd4d35a 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt @@ -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> = - 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> = 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> = discoveredDevicesFlow .map { it?.recentTcpDevices ?: emptyList() } + .distinctUntilChanged() .stateInWhileSubscribed(initialValue = emptyList()) val selectedAddressFlow: StateFlow = radioInterfaceService.currentDeviceAddressFlow diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt index f30d209cb..3bec4b188 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt @@ -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 diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/AnimatedConnectionsNavIcon.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/AnimatedConnectionsNavIcon.kt index 168196b0d..057924b73 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/AnimatedConnectionsNavIcon.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/AnimatedConnectionsNavIcon.kt @@ -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), + ) } } diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index 40aa14ed2..c8f94c47b 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -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) diff --git a/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt index c3e986d7d..9c2df6e2a 100644 --- a/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt +++ b/feature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt @@ -15,7 +15,6 @@ * along with this program. If not, see . */ @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), diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index 47cd22ca1..4b26bd1c3 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -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) } diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index af37fd6b3..c87dc492f 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -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) diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index cfe010cea..51f68a61c 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -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) diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts index 08e2f736a..c7730d00b 100644 --- a/feature/node/build.gradle.kts +++ b/feature/node/build.gradle.kts @@ -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) diff --git a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt index 2b1a39fd4..fb6d9710f 100644 --- a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt @@ -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()) { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt index 842a04110..eca12df89 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt @@ -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, ) diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index ac0505076..ea27b3e08 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -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) diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt index 67fe5878a..e3966f3d3 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt @@ -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) }, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cea6624ac..3716630dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/mesh_service_example/build.gradle.kts b/mesh_service_example/build.gradle.kts index 8b083656a..300a2efce 100644 --- a/mesh_service_example/build.gradle.kts +++ b/mesh_service_example/build.gradle.kts @@ -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)