Screens need hot streams. A StateFlow<T> always holds a value, hands it to each new collector and skips duplicates, much like React 7,897 state; Compose 234 observes it (ViewModel and UDF). A SharedFlow<T> broadcasts events such as toasts, replaying none by default. stateIn turns a cold flow into state:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class CartModel(scope: CoroutineScope) {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow() // read-only for the UI
private val _events = MutableSharedFlow<String>()
val events: SharedFlow<String> = _events.asSharedFlow() // one-off messages
val badge = count.map { "$it in cart" }.stateIn(scope, SharingStarted.Eagerly, "empty")
suspend fun add(b: Book) { _count.update { it + 1 }; _events.emit("Added ${b.title}") }
}
fun main() = runBlocking {
val cart = CartModel(this)
launch { cart.count.collect { log("count = $it") } }
launch { cart.events.collect { log("toast: $it") } }
delay(10); cart.add(catalog[2]); cart.add(catalog[5]); delay(10)
log("badge = ${cart.badge.value}")
coroutineContext.cancelChildren() // collectors never complete
}Output
0 ms [main @coroutine#3] count = 0 108 ms [main @coroutine#3] count = 1 114 ms [main @coroutine#4] toast: Added Salt and Saffron 117 ms [main @coroutine#3] count = 2 125 ms [main @coroutine#4] toast: Added Gardens in Glass 142 ms [main @coroutine#1] badge = 2 in cart
update { } is atomic. In a ViewModel, prefer SharingStarted.WhileSubscribed(5_000), which survives a rotation. Other everyday operators: debounce, combine, flatMapLatest, catch and retry.