kotlin-flow-state-event-modeling
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.
What this skill does
# Kotlin Flow: state and event modeling
## Core principle
**Pick the primitive that matches replay, fan-out, and synchronous-read requirements.** `StateFlow`, `SharedFlow`, `Channel`-backed flows, and cold `Flow` differ in buffering, who sees each emission, and whether `.value` exists. Wrong choices drop events, leak sharing coroutines, or force fake domain sentinels into state.
## When to use this skill
You're writing or reviewing Kotlin code involving:
- `MutableStateFlow<T>(SomeSentinel)` — `NoUser`, `Empty`, `Loading`, etc. — because the real value is async
- `.stateIn(...)` called inside a function rather than assigned to a property
- `SharingStarted.WhileSubscribed(...)` on a flow whose `.value` is read synchronously and must stay fresh
- `MutableSharedFlow` for navigation events, snackbars, or other one-shot emissions where loss would be a bug
- `.map { }` on a `StateFlow` when consumers still need synchronous `.value`
- `MutableStateFlow.value = _state.value.copy(...)` or update code that builds expensive objects inside `update { ... }`
## SharedFlow for single-consumer fire-once events
`SharedFlow` defaults have no replay buffer. If nothing is collecting at the exact instant of emission, the event is gone. For a **single UI consumer** handling exactly-once events such as navigation or snackbars, a buffered `Channel` exposed as a `Flow` often matches the semantics better:
```kotlin
// ❌ 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()` is **fan-out, not broadcast**: with multiple collectors, each event is delivered to **one** collector. `Channel.BUFFERED` is bounded, so sends can suspend and `trySend` can fail. If multiple observers must all see the same event, use explicit state, durable storage, or a deliberately configured `SharedFlow` instead.
## StateFlow polluted with invalid sentinel defaults
`StateFlow` forces an initial value. When the real value is async, developers sometimes invent fake domain values — `NoUser`, `EmptyUser`, placeholder IDs — and every consumer is forced to treat that sentinel as real data.
```kotlin
// ❌ 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() } }
}
```
One fix is **phasing**: don't expose the `StateFlow` until the real value exists.
```kotlin
// ✅ 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())
}
}
```
If absence, loading, or error is a real state, model it explicitly (`User?`, `sealed interface UserUiState`, `Result`, etc.). The bug is a fake domain value masquerading as real data, not every initial value.
## Mutate MutableStateFlow with `update { ... }`
Prefer `MutableStateFlow.update { current -> ... }` over reading `.value` and writing it back. `update` applies the transform atomically against the latest state, which avoids lost updates when multiple coroutines mutate the same state.
```kotlin
// 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,
)
}
```
Keep object creation outside the `update` block unless it needs the current state. The update lambda can be retried, so expensive work or side effects inside it may run more than once:
```kotlin
// 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)
}
```
The block should be a pure, fast state transformation: no network calls, database writes, logging side effects, random IDs, or time reads unless those values were captured before the block.
## `stateIn()` inside a function
```kotlin
// ❌ BAD — new sharing coroutine every call
fun getPreferences(): StateFlow<Prefs> =
repo.prefsFlow.stateIn(scope, SharingStarted.Eagerly, Prefs.Default)
```
Every call to `getPreferences()` launches a fresh coroutine on `scope` that never completes. Performance dies fast under repeated reads.
```kotlin
// ✅ GOOD — one shared instance, computed once
val preferences: StateFlow<Prefs> =
repo.prefsFlow.stateIn(viewModelScope, SharingStarted.Eagerly, Prefs.Default)
```
## `WhileSubscribed` with synchronous `.value`
`SharingStarted.WhileSubscribed(timeout)` disconnects the upstream when there are no active collectors. While disconnected, `.value` returns the last cached value, which may be stale or still the initial value.
**Rule:** if `.value` must be fresh or initialized without an active collector, use `SharingStarted.Eagerly` or explicit initialization. `WhileSubscribed` is fine when stale/cached values are acceptable and consumers primarily collect asynchronously.
## `.map` on `StateFlow` loses `.value`
```kotlin
// ❌ BAD — `name.value` won't compile; it's now a plain Flow
val name: Flow<String> = userState.map { it.name }
```
If you need synchronous `.value`, terminate the chain with `.stateIn(...)`:
```kotlin
// ✅ GOOD
val name: StateFlow<String> = userState
.map { it.name }
.stateIn(viewModelScope, SharingStarted.Eagerly, userState.value.name)
```
Community “derived state flow” utilities run the transform on every `.value` read — only acceptable for fast, idempotent transforms. Default to `.stateIn(...)`.
## Decision: which Flow type?
| Need | Primitive |
|------|-----------|
| State that always has a value, read by both async collectors **and** synchronous code | `StateFlow`, often with `SharingStarted.Eagerly` when `.value` matters |
| Hot stream, multiple subscribers, **no** requirement for synchronous `.value` | `SharedFlow` |
| Discrete events for **one** consumer, exactly-once handoff | Consider `Channel(BUFFERED).receiveAsFlow()` |
| Cold stream, one consumer per collection | Plain `Flow` |
If you're tempted to reach for `SharedFlow`, ask: would dropping an emission be a bug, and how many consumers must see it? If one consumer must handle it exactly once, a `Channel` may fit. If every observer must see it, model durable state or configure a broadcast stream deliberately.
## Quick reference
| Symptom | Problem | Fix |
|---------|---------|-----|
| `MutableStateFlow<X>(FakeDomainValue)` | Invalid placeholder default | Model absence explicitly or use phase initialization |
| `MutableSharedFlow<Event>` for single-consumer nav/snackbar | Lossy default event stream | Consider `Channel(BUFFERED).receiveAsFlow()` |
| `fun foo() = flow.stateIn(...)` | Per-call sharing coroutine | Make it a `val` / shared instance |
| `WhileSubscribed` + `.value` must be fresh/initialized | Stale or initial data | `SharingStarted.Eagerly` or explicit initialization |
| `stateFlow.map { ... }` consumed as state | Lost `.value` | Terminate with `.stateIn(...)` |
| `_state.value = _state.value.copy(...)` | Non-atomic read/modify/write | `_state.update { it.copy(...) }` |
| Expensive object creation inside `update { ... }` that doesn't use current state | Work can repeat if update retries | Build before `update`; keep only current-state transforms inside |
## Red flags during review
| Thought | ReaRelated in Writing & Docs
jax-development
IncludedUse this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
nature-article-writer
IncludedDrafts, rewrites, diagnostically critiques, and style-calibrates primary research manuscripts for Nature and Nature Portfolio journals. Use when the user wants a Nature-style title, summary paragraph or abstract, introduction, results, discussion, methods, figure legends, presubmission enquiry, cover letter, reviewer response, or when a scientific draft sounds generic, jargon-heavy, structurally weak, or AI-ish and needs precise, broad-reader-friendly prose without inventing data, analyses, or references. Best for primary research articles and letters rather than reviews or press releases unless explicitly adapting one.
deckrd
IncludedDocument-driven framework that derives requirements, specifications, implementation plans, and executable tasks from goals through structured AI dialogue. Use when user says "write requirements", "create spec", "plan implementation", "derive tasks", "structure this feature", "break down into tasks", or "document this module". Also use for reverse engineering existing code into docs (/deckrd rev). Do NOT use for direct code writing — use /deckrd-coder after tasks are generated. Do NOT use when the user only wants to run or fix existing code without planning.
clinical-decision-support
IncludedGenerate professional clinical decision support (CDS) documents for pharmaceutical and clinical research settings, including patient cohort analyses (biomarker-stratified with outcomes) and treatment recommendation reports (evidence-based guidelines with decision algorithms). Supports GRADE evidence grading, statistical analysis (hazard ratios, survival curves, waterfall plots), biomarker integration, and regulatory compliance. Outputs publication-ready LaTeX/PDF format optimized for drug development, clinical research, and evidence synthesis.
handling-sf-data
IncludedSalesforce data operations with 130-point scoring. Use this skill to create, update, delete, bulk import/export, generate test data, and clean up org records using sf CLI and anonymous Apex. TRIGGER when: user creates test data, performs bulk import/export, uses sf data CLI commands, needs data factory patterns for Apex tests, or needs to seed/clean records in a Salesforce org. DO NOT TRIGGER when: SOQL query writing only (use querying-soql), Apex test execution (use running-apex-tests), or metadata deployment (use deploying-metadata).
accelint-ac-to-playwright
IncludedConvert and validate acceptance criteria for Playwright test automation. Use when user asks to (1) review/evaluate/check if AC are ready for automation, (2) assess if AC can be converted as-is, (3) validate AC quality for Playwright, (4) turn AC into tests, (5) generate tests from acceptance criteria, (6) convert .md bullets or .feature Gherkin files to Playwright specs, (7) create test automation from requirements. Handles both bullet-style markdown and Gherkin syntax with JSON test plan generation and validation.