Skip to content

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.

store.state.collect { state -> render(state) }

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:

store.actions.collect { action -> logger.d("dispatched: $action") }

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.

if (store.isActive) {
    store.dispatch(MyAction.Sync)
}

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.

button.setOnClickListener { store.dispatch(MyAction.Submit) }

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.