Skip to content
Development
Skill

/kotlin-flow-state-event-modeling

Use when writing or reviewing Kotlin StateFlow/SharedFlow/Channel choices, sentinel default values, stateIn placement, WhileSubscribed staleness, or MutableStateFlow update patterns. Technique-layer skill — complements the codebase-specific kotlin-expert.

From plugin
amethyst
1.6k30 skills3 commands
Install
$ npx -y skills add vitorpamplona/amethyst --skill kotlin-flow-state-event-modeling --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/kotlin-flow-state-event-modeling

Context preview

The summary Claude sees to decide when to auto-load this skill.

Use when writing or reviewing Kotlin StateFlow/SharedFlow/Channel choices, sentinel default values, stateIn placement, WhileSubscribed staleness, or MutableStateFlow update patterns. Technique-layer skill — complements the codebase-specific kotlin-expert.

SKILL.md

kotlin-flow-state-event-modeling.SKILL.md
name: kotlin-flow-state-event-modeling
description: Use when writing or reviewing Kotlin StateFlow/SharedFlow/Channel choices, sentinel default values, stateIn placement, WhileSubscribed staleness, or MutableStateFlow update patterns. Technique-layer skill — complements the codebase-specific kotlin-expert.

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:

// ❌ 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.

// ❌ 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.

// ✅ 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.

// 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:

// 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

// ❌ 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.

// ✅ 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`

// ❌ BAD —
Read more
Ships withamethyst

Nostr client for Android

Get the whole plugin
Stats
1,599
Stars
221
Forks
Active
Maintenance
Kotlin
Language
MIT
License
8h ago
Last commit
3y ago
Created

Repo: vitorpamplona/amethyst

Other skills on amethyst.