Skip to content

perf(TableScreen): reduce recompositions via derivedStateOf, stable LazyColumn keys, and primitive state specialization #1371

@SamuelRT123

Description

@SamuelRT123

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions