Skip to content

Recipe: Observe a Flow or repository without leaking it

Problem: your presenter needs to turn a Flow (from a repository, database, or use-case) into Circuit state — without leaking the Flow or rebuilding it on every recomposition.

Use produceRetainedState and build the Flow inside the block. The result is retained across recomposition, configuration changes, and the back stack. The Flow is collected only while the block is active.

@Composable
override fun present(): ChannelState {
  val messages by produceRetainedState<List<Message>>(emptyList(), screen.channelId) {
    messageRepository.messages(screen.channelId).collect { latest -> value = latest }
  }
  return ChannelState(messages)
}

Pass any keys that should restart collection (here, screen.channelId) as arguments — the block re-runs when a key changes.

Don’t pass the Flow as an argument

collectAsRetainedState works, but it takes the Flow as a parameter. An inline repository.messages() expression and its operator chain are rebuilt in composition on every recomposition:

// ⚠️ Rebuilds the chain in composition every recomposition.
val messages by messageRepository.messages(screen.channelId)
  .map { messages -> messages.sortedByDescending(Message::timestamp) }
  .collectAsRetainedState(initial = emptyList())

// ✅ Built once, inside the block, keyed on channelId.
val messages by produceRetainedState<List<Message>>(emptyList(), screen.channelId) {
  messageRepository.messages(screen.channelId)
    .map { messages -> messages.sortedByDescending(Message::timestamp) }
    .collect { sorted -> value = sorted }
}

collectAsRetainedState is fine when the Flow is already a stable reference. produceRetainedState is the safer default for repository calls and operator chains.

Never rememberRetained a raw Flow instance

// Retains the Flow itself, including anything it captures.
val messages by rememberRetained { messageRepository.messages(id) }.collectAsState(emptyList())

Retain the result, not the Flow. Building a chain inside remember is fine because it runs once, but do not hold the Flow as retained state. The same applies to a Navigator or Context.

See also: Retention reference · Keep UI state across config change