Recipe: Share selection state across list items¶
Problem: long-pressing a list row enters Gmail-style bulk-selection. Every row must know selection mode is active (even though another row triggered it) and show a checkbox.
Rows in selection mode are not independent; they share live state. Keep that state in one parent presenter and render rows as plain composables. If per-row logic gets heavy, use a StateProducer.
data class InboxState(
val rows: List<RowState>,
val inSelectionMode: Boolean,
val eventSink: (InboxEvent) -> Unit,
) : CircuitUiState
data class RowState(val id: MessageId, val subject: String, val selected: Boolean)
sealed interface InboxEvent : CircuitUiEvent {
data class LongPress(val id: MessageId) : InboxEvent
data class Toggle(val id: MessageId) : InboxEvent
data object ClearSelection : InboxEvent
}
The parent presenter owns the selection set; every row’s selected flag is derived from it, so a
change made by one row is reflected in all of them on the next state emission:
@Composable
override fun present(): InboxState {
val messages by produceRetainedState<List<Message>>(emptyList()) {
inboxRepository.messages().collect { latest -> value = latest }
}
val selected = rememberRetained { mutableStateSetOf<MessageId>() }
// Rebuild rows only when messages or selection changes.
val rows by remember(messages) {
derivedStateOf {
messages.map { message ->
RowState(message.id, message.subject, selected = message.id in selected)
}
}
}
return InboxState(rows = rows, inSelectionMode = selected.isNotEmpty()) { event ->
when (event) {
is InboxEvent.LongPress -> selected.add(event.id)
is InboxEvent.Toggle ->
if (event.id in selected) selected.remove(event.id) else selected.add(event.id)
InboxEvent.ClearSelection -> selected.clear()
}
}
}
Rows read their selected flag and report events upward:
@Composable
private fun MessageRow(row: RowState, inSelectionMode: Boolean, eventSink: (InboxEvent) -> Unit) {
Row(
Modifier.combinedClickable(
onClick = { if (inSelectionMode) eventSink(InboxEvent.Toggle(row.id)) /* else open */ },
onLongClick = { eventSink(InboxEvent.LongPress(row.id)) },
)
) {
if (inSelectionMode) Checkbox(checked = row.selected, onCheckedChange = null)
Text(row.subject)
}
}
Rule of thumb: if children need shared state, model that state in the parent.
See also: Presenter patterns: StateProducer ยท Embed a reusable component