From 3fe1deb01ccbc17b2d08941cd3dfed7b747dd325 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:23:12 -0500 Subject: [PATCH] perf(node): add stable keys and contentType to telemetry chart lists (#5869) Co-authored-by: Claude Opus 4.8 --- .../meshtastic/core/ui/qr/ScannedQrCodeDialog.kt | 5 ++++- .../feature/node/metrics/AirQualityMetrics.kt | 6 +++++- .../feature/node/metrics/DeviceMetrics.kt | 12 ++++++++++-- .../feature/node/metrics/EnvironmentMetrics.kt | 6 +++++- .../feature/node/metrics/PaxMetrics.kt | 6 +++++- .../feature/node/metrics/PositionLogScreens.kt | 6 +++++- .../feature/node/metrics/PowerMetrics.kt | 6 +++++- .../feature/node/metrics/SignalMetrics.kt | 16 +++++++++++++++- 8 files changed, 54 insertions(+), 9 deletions(-) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt index a96617957..ed5f8c9f9 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt @@ -202,7 +202,10 @@ fun ScannedQrCodeDialog( ) } - itemsIndexed(channelSet.settings) { index, channel -> + itemsIndexed(channelSet.settings, key = { index, _ -> index }, contentType = { _, _ -> "channel" }) { + index, + channel, + -> val isExisting = !shouldReplace && index < channels.settings.size val channelObj = Channel(channel, channelSet.lora_config ?: Channel.default.loraConfig) ChannelSelection( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt index 03504af65..047026f90 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt @@ -166,7 +166,11 @@ fun AirQualityMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Uni }, listPart = { modifier, selectedX, lazyListState, onCardClick -> LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) { - itemsIndexed(data) { _, telemetry -> + itemsIndexed( + data, + key = { _, telemetry -> telemetry.time }, + contentType = { _, _ -> "air_quality_metrics" }, + ) { _, telemetry -> AirQualityMetricsCard( telemetry = telemetry, isSelected = telemetry.time.toDouble() == selectedX, 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 8aa1569bd..74db19032 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 @@ -191,7 +191,11 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { }, listPart = { modifier, selectedX, lazyListState, onCardClick -> LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) { - itemsIndexed(data) { _, telemetry -> + itemsIndexed( + data, + key = { _, telemetry -> telemetry.time }, + contentType = { _, _ -> "device_metrics" }, + ) { _, telemetry -> DeviceMetricsCard( telemetry = telemetry, isSelected = telemetry.time.toDouble() == selectedX, @@ -560,7 +564,11 @@ private fun DeviceMetricsScreenPreview() { /* Device Metric Cards */ LazyColumn(modifier = Modifier.fillMaxSize()) { - itemsIndexed(telemetries) { _, telemetry -> + itemsIndexed( + telemetries, + key = { _, telemetry -> telemetry.time }, + contentType = { _, _ -> "device_metrics" }, + ) { _, telemetry -> DeviceMetricsCard(telemetry = telemetry, isSelected = false, onClick = {}) } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt index bb2debd45..de6a01c97 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt @@ -115,7 +115,11 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Un }, listPart = { modifier, selectedX, lazyListState, onCardClick -> LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) { - itemsIndexed(filteredTelemetries) { _, telemetry -> + itemsIndexed( + filteredTelemetries, + key = { _, telemetry -> telemetry.time }, + contentType = { _, _ -> "environment_metrics" }, + ) { _, telemetry -> EnvironmentMetricsCard( telemetry = telemetry, environmentDisplayFahrenheit = state.isFahrenheit, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt index 7a44405ab..d88c19e03 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt @@ -231,7 +231,11 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Uni state = lazyListState, contentPadding = PaddingValues(horizontal = 16.dp), ) { - itemsIndexed(paxMetrics) { _, (log, pax) -> + itemsIndexed( + paxMetrics, + key = { _, (log, _) -> log.uuid }, + contentType = { _, _ -> "pax_metrics" }, + ) { _, (log, pax) -> PaxMetricsItem( log = log, pax = pax, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt index a52f046c5..86a1c2589 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt @@ -70,7 +70,11 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { }, listPart = { modifier, selectedX, lazyListState, onCardClick -> LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) { - itemsIndexed(positions) { _, position -> + itemsIndexed( + positions, + key = { _, position -> position.time }, + contentType = { _, _ -> "position_log" }, + ) { _, position -> PositionCard( position = position, displayUnits = state.displayUnits, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt index 006acf37e..10df7d752 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt @@ -159,7 +159,11 @@ fun PowerMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { }, listPart = { modifier, selectedX, lazyListState, onCardClick -> LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) { - itemsIndexed(data) { _, telemetry -> + itemsIndexed( + data, + key = { _, telemetry -> telemetry.time }, + contentType = { _, _ -> "power_metrics" }, + ) { _, telemetry -> PowerMetricsCard( telemetry = telemetry, isSelected = telemetry.time.toDouble() == selectedX, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt index 7e0bf083b..77a152cac 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt @@ -114,12 +114,22 @@ private val LEGEND_DATA = private sealed interface SignalLogEntry { val timeSeconds: Int + /** Stable, collision-free identity for use as a LazyColumn item key across both entry types. */ + val key: Any + + /** Distinguishes the two card layouts so Compose can reuse compositions per type. */ + val contentType: Any + data class LocalStatsEntry(val telemetry: Telemetry) : SignalLogEntry { override val timeSeconds: Int = telemetry.time + override val key: Any = "local_stats_${telemetry.time}" + override val contentType: Any = "local_stats" } data class PacketEntry(val meshPacket: MeshPacket) : SignalLogEntry { override val timeSeconds: Int = meshPacket.rx_time + override val key: Any = "packet_${meshPacket.id}" + override val contentType: Any = "signal_packet" } } @@ -204,7 +214,11 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit, m } } else { LazyColumn(modifier = contentModifier.fillMaxSize(), state = lazyListState) { - itemsIndexed(data) { _, entry -> + itemsIndexed( + data, + key = { _, entry -> entry.key }, + contentType = { _, entry -> entry.contentType }, + ) { _, entry -> when (entry) { is SignalLogEntry.LocalStatsEntry -> LocalStatsCard(