Actions
Actions perform work and emit events back to the formula — observing streams, running
async operations, and responding to lifecycle. Declared within evaluate(), the runtime
manages their lifecycle: starting them when declared, cancelling them when removed.
override fun Snapshot<Input, State>.evaluate(): Evaluation<Output> {
return Evaluation(
output = ...,
actions = context.actions {
// actions are declared here
Action.fromFlow { repository.observeUser(userId) }.onEvent { user ->
transition(state.copy(user = user))
}
}
)
}
Integrating Coroutine actions
For suspend functions that return a single result, use Action.launch:
Action.launch { fetchUser(userId) }.onEvent { user ->
// Update state
transition(state.copy(user = user))
}
To handle errors, use Action.launchCatching which wraps the result in kotlin.Result:
Action.launchCatching { fetchUser(userId) }.onEvent { result ->
val newState = if (result.isSuccess) {
state.copy(user = result.getOrNull())
} else {
state.copy(error = result.exceptionOrNull())
}
transition(newState)
}
To start and collect Flow events:
Action.fromFlow { repository.observeUser(userId) }.onEvent { user ->
// Update state
transition(state.copy(user = user))
}
Responding to lifecycle events
Emits when action is initialized:
Action.onInit().onEvent {
transition { analytics.trackScreenOpen() }
}
Emits when action is terminated (state transitions are discarded, only side effects):
Action.onTerminate().onEvent {
transition { analytics.trackCloseEvent() }
}
Emits data on initialization and re-emits whenever data changes. This uses the key
mechanism — the data is the key, so when it changes the runtime restarts the action:
Action.onData(itemId).onEvent {
transition { analytics.trackItemLoaded(itemId) }
}
Action Lifecycle
evaluate() is called many times over a formula's lifetime. Actions need to persist across
these evaluations — the runtime starts them when first declared, keeps them running across
re-evaluations, and cancels them when no longer declared.
Using Keys
To match actions across evaluations, the runtime uses a composite key (positional key
based on anonymous class type + optional user-provided key). During evaluation, each
action declaration looks up its key in LifecycleCache — if a matching action exists,
it is reused; if not, a new one is started. Actions not declared during an evaluation
are cancelled afterward.
The key parameter enables distinguishing between different actions. If the key changes,
the runtime cancels the old action and starts a new one.
Action.fromFlow(key = input.taskId) { taskRepo.fetchTask(input.taskId) }.onEvent { taskResponse ->
transition(state.copy(task = taskResponse))
}
If input.taskId changes, the runtime cancels the old flow and starts a new one.
Conditional Logic
Since the runtime manages actions based on what's declared in evaluation, conditional logic controls when actions run.
if (state.locationTrackingEnabled) {
Action.fromFlow { locationManager.updates() }.onEvent { event ->
transition(state.copy(location = event.location))
}
}
If state.locationTrackingEnabled changes from true to false, the action is no
longer declared and the runtime cancels it.