From 228fa8a368d478a9a244da2eebaef812ae724df5 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 10 Jul 2026 08:31:54 -0500 Subject: [PATCH] fix: dedup node lists to prevent LazyColumn duplicate-key crash (#6193) Co-authored-by: Claude Opus 4.8 --- .../meshtastic/app/map/component/ClusterItemsListDialog.kt | 4 +++- .../org/meshtastic/feature/node/list/NodeListScreen.kt | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt index d69e3f316..18226a0bb 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt @@ -48,7 +48,9 @@ fun ClusterItemsListDialog( text = { // Use a LazyColumn for potentially long lists of items LazyColumn(contentPadding = PaddingValues(vertical = 8.dp)) { - items(items, key = { it.node.num }) { item -> + // Dedup by node.num: it's the LazyColumn key and must be unique, but filteredNodes can + // contain the same node from two sources (or a num=0 placeholder). See NodeListScreen. + items(items.distinctBy { it.node.num }, key = { it.node.num }) { item -> ClusterDialogListItem(item = item, onClick = { onItemClick(item) }) } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt index 8397df596..8fee0fa3c 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt @@ -106,7 +106,12 @@ fun NodeListScreen( val scope = rememberCoroutineScope() val state by viewModel.nodesUiState.collectAsStateWithLifecycle() - val nodes by viewModel.nodeList.collectAsStateWithLifecycle() + // Dedup by num: it's the LazyColumn key (must be unique) but the upstream flow can momentarily emit + // two entries with the same num (a num=0 unknown-node placeholder, or a brief double-emission during + // an active-DB switch). Dedup once here so the count in the app bar and the rendered rows agree. + // Composite "num_index" keys would also fix the crash but break animateItem() reorder animations. + val allNodes by viewModel.nodeList.collectAsStateWithLifecycle() + val nodes = remember(allNodes) { allNodes.distinctBy { it.num } } val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle() val onlineNodeCount by viewModel.onlineNodeCount.collectAsStateWithLifecycle(0) val totalNodeCount by viewModel.totalNodeCount.collectAsStateWithLifecycle(0)