Store API reference¶
Store<State, Action, Effect> is the public contract for every running state machine instance,
regardless of how it was created (store { }, StateMachineStore, etc.).
Properties¶
id: String¶
A unique identifier for this store instance. Auto-generated as a UUID by default. Used by
StoreRegistry to distinguish multiple instances of the same store class, and by
RelayScope.dispatch(…, id = …) to target a specific instance:
// Target one specific CartStore instance out of several registered:
dispatch(CartStore::class, CartAction.Clear, id = specificCartId)
name: String¶
A human-readable name for this store. Set via the name(…) call in the builder DSL:
val store = store<MyState, MyAction, MyEffect>(scope) {
name("Login")
initialState(MyState.Idle)
// …
}
For stores created via StateMachineStore subclasses, name defaults to the subclass's simple
class name (e.g., "LoginStateMachine") when not explicitly set. For inline store { } calls,
it defaults to an empty string unless set.
name is surfaced in StoreRegistry.PluginScope to make per-store plugin tagging ergonomic:
StoreRegistry(viewModelScope) {
install { LoggingPlugin(tag = name) } // name = store.name ?: class.simpleName ?: id
}
state: StateFlow<State>¶
The current state, exposed as a StateFlow. Always holds a value; the initial emission is the
configured initialState.
Collecting state also calls start() implicitly — the store's onEnter for the initial state
fires the first time a subscriber attaches.
effects: SharedFlow<Effect>¶
One-shot side effects, exposed as a SharedFlow with replay = 0. Late subscribers miss effects
emitted before they subscribed.
Subscription ordering warning: collecting state implicitly calls start(), which fires the
initial onEnter hook. Any effects emitted by that hook are lost if no subscriber is attached to
effects yet. Always subscribe to effects before subscribing to state, or call start()
explicitly only after all subscribers are attached:
// Safe — effects subscriber is ready before state collection triggers start()
launch { store.effects.collect { handle(it) } }
launch { store.state.collect { render(it) } }
For Compose, use handleEffects { } from monaka-compose — it wires up subscription ordering
automatically. See Compose integration.
Backpressure: if no subscriber is consuming effects and the internal buffer fills up, the processing coroutine suspends and all state transitions stall. Ensure at least one subscriber consumes effects promptly.
actions: SharedFlow<Action>¶
Every action dispatched to the store, emitted in dispatch order before the action is
processed. replay = 0 — late subscribers miss past actions.
Primary use-case is relaying: the relay { action<A> { … } } DSL subscribes to this flow
internally. You can also use it for debug logging or analytics:
isActive: Boolean¶
true while the store is processing actions; false after stop() is called or the owning
CoroutineScope is canceled. All write operations (dispatch, onLifecycleEvent) are silent
no-ops when isActive is false.
Note: isActive is an abstract property on the Store interface — custom Store implementations
must provide a concrete override. Forgetting to do so is a compile-time error.
Functions¶
dispatch(action: Action)¶
Enqueue an action for processing. Non-suspending and safe to call from any thread or coroutine. Actions are processed sequentially in the order they are enqueued.
start()¶
Fire the onEnter hook for the initial state, if one is registered.
When an initializer was provided at store construction, start() enqueues the async restore
first. The initializer runs inside the processing coroutine before onEnter and before any
queued actions, so the machine always sees the restored state as its first state.
start() is called automatically the first time a subscriber collects state, actions, or
effects. Call it explicitly when you need onEnter to fire before any collector attaches —
for example, in a background ViewModel that starts work immediately on creation:
class SyncViewModel : ViewModel() {
val store = store<SyncState, SyncAction, SyncEffect>(viewModelScope) {
initialState(SyncState.Idle)
state<SyncState.Idle> {
onEnter { dispatch(SyncAction.StartSync) }
}
}
init {
store.start() // onEnter fires immediately; no UI subscriber needed
}
}
Calling start() more than once is a safe no-op. Calling it after stop() is also a no-op.
stop()¶
Stop the store permanently. Cancels the internal processing coroutine and all running keyed jobs.
Closes the trigger channel. All subsequent calls to dispatch and onLifecycleEvent become
silent no-ops.
Note that stop() does not fire callbacks registered via invokeOnCompletion — those are
attached to the owning CoroutineScope and only fire when the scope is canceled. If you stop a
store early (before its scope is canceled) and the store is registered in a StoreRegistry, call
unregister manually:
// Composition entry leaving the nav stack
DisposableEffect(viewModel) {
onDispose {
viewModel.store.stop()
registry.unregister(viewModel.store)
}
}
On Android, prefer letting the owning CoroutineScope (e.g. viewModelScope) stop the store
automatically when the ViewModel is cleared — scope cancellation triggers invokeOnCompletion and
auto-unregistration. Call stop() explicitly only when the store has a shorter lifetime than its
scope.
onLifecycleEvent(event: LifecycleEvent)¶
Forward an application lifecycle event into the machine. The event is enqueued in the same channel as actions and processed sequentially. See Lifecycle hooks for the full list of events and how to react to them in the DSL.
triggerStateHook(hook: StateHook<State>)¶
Fire a state lifecycle hook (OnEnter, OnExit, or OnUpdate) directly, without requiring a
transition. Annotated @InternalMonakaApi — calling it outside of test infrastructure requires
@OptIn(InternalMonakaApi::class). In practice this is handled automatically by :monaka-test,
which calls it on your behalf via trigger(StateHook.OnEnter) { … }. See
Testing for usage.
install(plugin: Plugin)¶
Attach a plugin to this store after construction. The plugin begins receiving events from the next processed action onward — events that occurred before this call are not replayed.
This is primarily used by StoreRegistry to apply global plugins retroactively to
already-registered stores. Calling it directly is possible but unusual; prefer install(…) in
the builder DSL for plugins that are always needed.
invokeOnCompletion(handler: (Throwable?) -> Unit): DisposableHandle¶
Register a callback that fires when the store's owning CoroutineScope is canceled. The handler
receives the cancellation cause, or null for normal completion. Useful for observing store
lifetime without holding a reference to the underlying scope:
val handle = store.invokeOnCompletion { cause ->
logger.d("store completed, cause=$cause")
}
// Later, if you want to remove the callback:
handle.dispose()
The callback fires when the store's owning CoroutineScope is canceled (e.g. viewModelScope
cleared on Android). It does not fire when stop() is called directly — stop() only
cancels the internal processing coroutine, not the scope.
This is how StoreRegistry.register implements auto-unregistration: it attaches an
invokeOnCompletion handler that calls stop() and unregister when the owning scope is
canceled. If you call stop() directly before the scope is canceled, call
registry.unregister(store) manually to remove it from the registry.