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)

You can read the id to log or correlate store activity:

install(object : Plugin<MyState, MyAction, MyEffect> {
    override fun onTransition(fromState: MyState, toState: MyState) {
        logger.d("store[$id] $fromState$toState")
    }
})

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. Use handleEffects { } (see Compose integration) or attach your collector before the first dispatch to avoid missing emissions.

store.effects.collect { effect -> handle(effect) }

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 cancelled. All write operations (dispatch, onLifecycleEvent) are silent no-ops when isActive is false.

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

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 cancelled. If you stop a store early (before its scope is cancelled) 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.


invokeOnCompletion(handler: (Throwable?) -> Unit): DisposableHandle

Register a callback that fires when the store's owning CoroutineScope is cancelled. 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 cancelled (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 cancelled. If you call stop() directly before the scope is cancelled, call registry.unregister(store) manually to remove it from the registry.