Key Strategy Trade‑off for LazyColumn with Paging 3 Placeholders
0 reputation · 03 Nov 2024, 03:13 UTC
0 reputation · 03 Nov 2024, 03:13 UTC
A LazyColumn backed by Paging 3 must supply a key lambda that preserves item identity across recompositions and configuration changes, especially when placeholders are enabled by default.
Using the domain ID (key = it.id) works only if the ID is immutable; Paging 3 inserts null placeholders, forcing a null‑safe fallback such as it?.id ?: -position, and RemoteMediator upserts can reuse IDs, causing diffing errors and duplicated composables.
Positional keys (key = index) avoid ID reuse and placeholder null handling, but they shift as pages load, breaking scroll position restoration after config changes or process death.
What null‑safe key strategy guarantees uniqueness for real IDs and placeholder indices? Does disabling placeholders improve scroll restoration despite initial gaps? Can a safely composed hybrid key avoid lambda mutation between recompositions?
35775 reputation · 03 Nov 2024, 12:50 UTC
Use a hybrid key that combines the domain ID with a monotonically increasing generation token:
val pagingState = remember { mutableStateOf(pager.flow.collectAsStateWithLifecycle().value) }
LazyColumn(
state = rememberLazyListState(),
key = { index, item ->
item?.let { "${it.id}#${pagingState.value?.generation ?: 0}" }
?: "placeholder#$index#${pagingState.value?.generation ?: 0}"
}
) { items(...) }This guarantees uniqueness for real items and placeholders, survives config changes, and avoids lambda mutation because the generation token is derived from immutable PagingState.
key = it.id)Fails because Paging 3 inserts null placeholders by default. A null-safe fallback like it?.id ?: -position creates collisions when RemoteMediator upserts reuse the same server ID across refreshes—Compose sees duplicate keys and throws.
key = index)Indices shift when preceding pages insert new items. After a config change or process death, LazyListState restores firstVisibleItemIndex, but that index now maps to a different logical item, causing scroll jumps.
PagingConfig(enablePlaceholders = false) eliminates nulls and lets you use key = it.id, but the initial render shows gaps until the first page loads. Scroll restoration still works because LoadState.NotLoading emits only after data arrives, but users see flicker on slow networks. A skeleton composable driven by LoadState.Loading is a better UX than placeholders.
refresh() or use PagingState.lastRefreshKey as the token.val pagingState = remember { mutableStateOf(pager.flow.collectAsStateWithLifecycle().value) } ensures the lambda captures a stable reference.pagingState.value.generation and the item—both stable across recompositions.key attribute matching the pattern id#gen or placeholder#index#gen.RemoteMediator.refresh(): no duplicate-key crashes, placeholders get fresh keys.LazyListState.scrollPosition before/after config change via onGloballyPositioned logging.Does your RemoteMediator implementation already expose a generation/load ID (e.g., via a refreshGeneration property), or do you need to derive one from PagingState.lastRefreshKey or a side-effect counter? The answer determines whether step 1 is a one-liner or requires a small wrapper.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.