Loading...
Loading...
Use when writing or reviewing Kotlin Flow state and event APIs with StateFlow, MutableStateFlow.update, SharedFlow, Channel, stateIn, SharingStarted, .value, receiveAsFlow, one-shot events, or sentinel initial values.
npx skill4agent add chrisbanes/skills kotlin-flow-state-event-modelingStateFlowSharedFlowChannelFlow.valueMutableStateFlow<T>(SomeSentinel)NoUserEmptyLoading.stateIn(...)SharingStarted.WhileSubscribed(...).valueMutableSharedFlow.map { }StateFlow.valueMutableStateFlow.value = _state.value.copy(...)update { ... }SharedFlowChannelFlow// ❌ BAD
private val _navEvents = MutableSharedFlow<NavigationEvent>()
val navEvents: SharedFlow<NavigationEvent> = _navEvents.asSharedFlow()
// ✅ GOOD
private val _navEvents = Channel<NavigationEvent>(Channel.BUFFERED)
val navEvents: Flow<NavigationEvent> = _navEvents.receiveAsFlow()Channel.receiveAsFlow()Channel.BUFFEREDtrySendSharedFlowStateFlowNoUserEmptyUser// ❌ BAD — sentinel leaks into the type
class UserSession(private val db: Db) {
private val _user = MutableStateFlow<User>(NoUser)
val user: StateFlow<User> = _user.asStateFlow()
init { scope.launch { _user.value = db.load() } }
}StateFlow// ✅ GOOD — bootstrap suspends; observers only see real users
class UserSession(private val db: Db) {
private var _user: MutableStateFlow<User>? = null
val user: StateFlow<User>
get() = checkNotNull(_user) { "Call login() first" }
suspend fun login() {
_user = MutableStateFlow(db.load())
}
}User?sealed interface UserUiStateResultupdate { ... }MutableStateFlow.update { current -> ... }.valueupdate// BAD — read/modify/write can lose concurrent updates.
_state.value = _state.value.copy(
selectedId = id,
details = details,
)
// GOOD — transform starts from the latest state.
_state.update { current ->
current.copy(
selectedId = id,
details = details,
)
}update// GOOD — details does not depend on current state, so build it once.
val details = Details.from(response)
_state.update { current ->
current.copy(details = details)
}
// GOOD — derived value depends on current state, so compute it inside.
_state.update { current ->
val nextItems = current.items.replaceById(updatedItem)
current.copy(items = nextItems)
}stateIn()// ❌ BAD — new sharing coroutine every call
fun getPreferences(): StateFlow<Prefs> =
repo.prefsFlow.stateIn(scope, SharingStarted.Eagerly, Prefs.Default)getPreferences()scope// ✅ GOOD — one shared instance, computed once
val preferences: StateFlow<Prefs> =
repo.prefsFlow.stateIn(viewModelScope, SharingStarted.Eagerly, Prefs.Default)WhileSubscribed.valueSharingStarted.WhileSubscribed(timeout).value.valueSharingStarted.EagerlyWhileSubscribed.mapStateFlow.value// ❌ BAD — `name.value` won't compile; it's now a plain Flow
val name: Flow<String> = userState.map { it.name }.value.stateIn(...)// ✅ GOOD
val name: StateFlow<String> = userState
.map { it.name }
.stateIn(viewModelScope, SharingStarted.Eagerly, userState.value.name).value.stateIn(...)| Need | Primitive |
|---|---|
| State that always has a value, read by both async collectors and synchronous code | |
Hot stream, multiple subscribers, no requirement for synchronous | |
| Discrete events for one consumer, exactly-once handoff | Consider |
| Cold stream, one consumer per collection | Plain |
SharedFlowChannel| Symptom | Problem | Fix |
|---|---|---|
| Invalid placeholder default | Model absence explicitly or use phase initialization |
| Lossy default event stream | Consider |
| Per-call sharing coroutine | Make it a |
| Stale or initial data | |
| Lost | Terminate with |
| Non-atomic read/modify/write | |
Expensive object creation inside | Work can repeat if update retries | Build before |
| Thought | Reality |
|---|---|
"We need | Multiple subscribers change the semantics. |
"We'll use | Only if stale/initial |
| "I'll use a sentinel until real data loads" | Consumers treat it as real domain; prefer explicit UI/state modeling or phasing. |
"I'll construct the new object inside | The lambda may retry. Construct outside unless it depends on the current state. |
kotlin-coroutines-structured-concurrencyrunBlockingcompose-side-effectscompose-state-holder-ui-split