Summary
This issue tracks three micro-optimizations identified in TableScreen.kt that
reduce unnecessary recompositions, GC pressure, and GPU overdraw during normal
table interaction (scrolling, selection mode, record editing).
These were surfaced through Android Studio's Layout Inspector (recomposition
counts), Perfetto GPU rendering traces, and Android Lint warnings.
Problem 1 — Unstable LazyColumn key forces full item recomposition on updates
File: TableScreen.kt
Location: LazyColumn { items(...) } block
The current item key is composed as:
items(tableData, key = { "${it.measurementId}_${it.timestamp}" }) { ... }
measurementId is already globally unique per row. Appending _${timestamp}
makes the key redundant and causes Compose's diffing algorithm to treat
re-ordered or refreshed items as entirely new nodes rather than moved ones,
triggering full recomposition of every visible item after any table update
(e.g. returning from the edit screen).
Expected: Only items whose data actually changed should recompose.
Problem 2 — resolvedSelectionCount recalculates on every recomposition
File: TableScreen.kt
Location: resolvedSelectionCount and resolvedCount inside
showDeleteConfirmDialog
Both use remember(selectedKeys, tableDataSnapshot) { ... }, which re-executes
the lambda on every recomposition of the surrounding scope — not only when
selectedKeys or aggItemByPeriodStart change. In selection mode with many
rows, this sumOf runs on every frame touching any state in the composable.
Expected: The count should only recalculate when the inputs it actually
reads change.
Problem 3 — mutableStateOf<Int?> autoboxes on every highlight change
File: TableScreen.kt
Location: highlightedItemId state declaration
var highlightedItemId by remember { mutableStateOf(null) }
Android Lint flags this pattern: "State<T> will autobox values assigned to
this state. Use a specialized state type instead."
Every assignment to highlightedItemId allocates a new Integer wrapper on
the heap, adding GC pressure during the post-edit scroll/highlight animation
where timing is frame-critical.
Expected: Use mutableIntStateOf with a sentinel value to eliminate the
wrapper allocation entirely.
Proposed Fix
1. Stable key in LazyColumn
// Before
items(tableData, key = { "${it.measurementId}_${it.timestamp}" }) { ... }
// After — rowKey() already exists in the file
items(tableData, key = { rowKey(it) }) { ... }
2. derivedStateOf for selection count
// Before
val resolvedSelectionCount = remember(selectedKeys, tableDataSnapshot) {
if (effectiveAggregationLevel == AggregationLevel.NONE)
selectedKeys.size
else
selectedKeys.sumOf { key ->
val periodStart = key.toLongOrNull() ?: return@sumOf 0
aggItemByPeriodStart[periodStart]?.aggregatedFromCount ?: 1
}
}
// After
val resolvedSelectionCount by remember {
derivedStateOf {
if (effectiveAggregationLevel == AggregationLevel.NONE)
selectedKeys.size
else
selectedKeys.sumOf { key ->
val periodStart = key.toLongOrNull() ?: return@sumOf 0
aggItemByPeriodStart[periodStart]?.aggregatedFromCount ?: 1
}
}
}
Same change applies to resolvedCount inside the showDeleteConfirmDialog block.
3. Primitive state specialization
// Before
var highlightedItemId by remember { mutableStateOf(null) }
// After — sentinel -1 signals "no highlight"; real IDs are always >= 0
var highlightedItemId by remember { mutableIntStateOf(-1) }
// Reset site:
highlightedItemId = -1 // instead of null
New import required:
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableIntStateOf
Impact
| Change |
Benefit |
Stable LazyColumn key |
Compose reuses existing item nodes on table refresh; only truly changed rows recompose |
derivedStateOf for count |
Selection count recalculates only when selectedKeys content changes, not on every frame |
mutableIntStateOf sentinel |
Eliminates Integer heap allocation on every highlight transition; reduces GC jitter during post-edit animation |
These optimizations are most noticeable on mid/low-end devices with large
datasets (100+ measurements) and when frequently entering/exiting selection mode.
References
Summary
This issue tracks three micro-optimizations identified in
TableScreen.ktthatreduce unnecessary recompositions, GC pressure, and GPU overdraw during normal
table interaction (scrolling, selection mode, record editing).
These were surfaced through Android Studio's Layout Inspector (recomposition
counts), Perfetto GPU rendering traces, and Android Lint warnings.
Problem 1 — Unstable
LazyColumnkey forces full item recomposition on updatesFile:
TableScreen.ktLocation:
LazyColumn { items(...) }blockThe current item key is composed as:
measurementIdis already globally unique per row. Appending_${timestamp}makes the key redundant and causes Compose's diffing algorithm to treat
re-ordered or refreshed items as entirely new nodes rather than moved ones,
triggering full recomposition of every visible item after any table update
(e.g. returning from the edit screen).
Expected: Only items whose data actually changed should recompose.
Problem 2 —
resolvedSelectionCountrecalculates on every recompositionFile:
TableScreen.ktLocation:
resolvedSelectionCountandresolvedCountinsideshowDeleteConfirmDialogBoth use
remember(selectedKeys, tableDataSnapshot) { ... }, which re-executesthe lambda on every recomposition of the surrounding scope — not only when
selectedKeysoraggItemByPeriodStartchange. In selection mode with manyrows, this
sumOfruns on every frame touching any state in the composable.Expected: The count should only recalculate when the inputs it actually
reads change.
Problem 3 —
mutableStateOf<Int?>autoboxes on every highlight changeFile:
TableScreen.ktLocation:
highlightedItemIdstate declarationAndroid Lint flags this pattern: "State<T> will autobox values assigned to
this state. Use a specialized state type instead."
Every assignment to
highlightedItemIdallocates a newIntegerwrapper onthe heap, adding GC pressure during the post-edit scroll/highlight animation
where timing is frame-critical.
Expected: Use
mutableIntStateOfwith a sentinel value to eliminate thewrapper allocation entirely.
Proposed Fix
1. Stable key in
LazyColumn2.
derivedStateOffor selection countSame change applies to
resolvedCountinside theshowDeleteConfirmDialogblock.3. Primitive state specialization
New import required:
Impact
LazyColumnkeyderivedStateOffor countselectedKeyscontent changes, not on every framemutableIntStateOfsentinelIntegerheap allocation on every highlight transition; reduces GC jitter during post-edit animationThese optimizations are most noticeable on mid/low-end devices with large
datasets (100+ measurements) and when frequently entering/exiting selection mode.
References
derivedStateOf