Actor
The Actor is a crucial component of the Store architecture, responsible for implementing the core logic of the application.
Overview
Actor is designed to:
- Accept
Intent - Produce
SideEffect - Generate new
State
It must be part of the Store and is managed by it.
Interface Definition
public interface Actor<Intent : Any, State : Any, out SideEffect : Any> {
/**
* Initializes the Actor
* Called by the Store
*/
public fun init(
scope: CoroutineScope,
getState: () -> State,
reduce: (State.() -> State) -> Unit,
onNewIntent: (Intent) -> Unit,
postSideEffect: (sideEffect: SideEffect) -> Unit,
)
/**
* Called when the Store receives a new Intent
* Called by the Store
*/
public fun onIntent(intent: Intent)
/**
* Destroys the Actor
* Called by the Store
*/
public fun destroy()
}
Key Methods
init
public fun init(
scope: CoroutineScope,
getState: () -> State,
reduce: (State.() -> State) -> Unit,
onNewIntent: (Intent) -> Unit,
postSideEffect: (sideEffect: SideEffect) -> Unit,
)
This function initializes the Actor. It is called by the Store and provides the Actor with necessary dependencies:
scope: ACoroutineScopefor managing coroutinesgetState: A function to retrieve the current statereduce: A function to update the stateonNewIntent: A function to handle new intentspostSideEffect: A function to emit side effects
onIntent
This function is called when the Store receives a new Intent. It's where the Actor processes incoming intents and updates the state or produces side effects accordingly.
destroy
This function is called to destroy the Actor. It should be used to clean up any resources or cancel any ongoing operations.
Actor Implementations
SimpleMVI provides three approaches for implementing actors:
1. DefaultActor - Object-Oriented Approach
DefaultActor is an abstract base class that provides a traditional OOP approach to implementing actors:
class CounterActor : DefaultActor<CounterIntent, CounterState, CounterSideEffect>() {
override fun handleIntent(intent: CounterIntent) {
when (intent) {
is CounterIntent.Increment -> {
reduce { copy(count = count + 1) }
}
is CounterIntent.Decrement -> {
reduce { copy(count = count - 1) }
}
is CounterIntent.Reset -> {
reduce { CounterState() }
sideEffect(CounterSideEffect.CounterReset)
}
}
}
override fun onInit() {
// Optional: initialization logic
}
override fun onDestroy() {
// Optional: cleanup logic
}
}
Available protected members:
state: State- Access to current statescope: CoroutineScope- Coroutine scope for async operationsreduce(block: State.() -> State)- Update statesideEffect(sideEffect: SideEffect)- Emit side effectintent(intent: Intent)- Dispatch new intent
When to use:
- Complex business logic requiring shared state or helper methods
- Large projects where OOP structure is beneficial
- When you prefer traditional class-based approach
- When you need to share logic between multiple intent handlers
2. actorDsl - Functional DSL Approach
The actorDsl function provides a declarative DSL for creating actors without defining a class:
val counterActor = actorDsl<CounterIntent, CounterState, CounterSideEffect> {
onInit {
// Optional: initialization logic
}
onIntent<CounterIntent.Increment> { intent ->
reduce { copy(count = count + 1) }
}
onIntent<CounterIntent.Decrement> { intent ->
reduce { copy(count = count - 1) }
}
onIntent<CounterIntent.Reset> { intent ->
reduce { CounterState() }
sideEffect(CounterSideEffect.CounterReset)
}
onDestroy {
// Optional: cleanup logic
}
}
Features:
- Type-safe intent handling using reified types
- Each intent type can have only one handler
- Handlers execute in the context of
ActorScope - Less boilerplate code
- Declarative style
When to use:
- Small to medium-sized projects
- Simple business logic without complex dependencies
- When you prefer functional style
- Prototyping and quick development
3. delegatedActor - Explicit Handler Composition
The delegatedActor function allows you to explicitly compose intent handlers, providing maximum flexibility:
val counterActor = delegatedActor<CounterIntent, CounterState, CounterSideEffect>(
initHandler = InitHandler {
// Optional: initialization logic
},
intentHandlers = listOf(
intentHandler<CounterIntent, CounterState, CounterSideEffect, CounterIntent.Increment> { intent ->
reduce { copy(count = count + 1) }
},
intentHandler<CounterIntent, CounterState, CounterSideEffect, CounterIntent.Decrement> { intent ->
reduce { copy(count = count - 1) }
},
intentHandler<CounterIntent, CounterState, CounterSideEffect, CounterIntent.Reset> { intent ->
reduce { CounterState() }
sideEffect(CounterSideEffect.CounterReset)
}
),
destroyHandler = DestroyHandler {
// Optional: cleanup logic
}
)
Features:
- Explicit handler registration
- Handlers can be defined separately and composed
- Useful for modular code organization
- Can be combined with code generation (see below)
When to use:
- When you need to compose handlers from different modules
- When handlers are defined separately
- When using code generation for intent handlers
- Large projects with complex handler composition
Code Generation for DelegatedActor
SimpleMVI provides KSP-based code generation to simplify working with delegatedActor and intent handlers.
@DelegatedStore Annotation
Annotate your Store with @DelegatedStore to generate type-safe handler factories:
@DelegatedStore
class CounterStore : Store<CounterStore.Intent, CounterStore.State, CounterStore.SideEffect> by createStore(
name = storeName<CounterStore>(),
initialState = State(counter = 0),
actor = delegatedActor(
initHandler = myInitHandler(),
intentHandlers = listOf(
incrementIntentHandler(),
decrementIntentHandler(),
),
destroyHandler = myDestroyHandler(),
)
) {
sealed interface Intent {
data object Increment : Intent
data object Decrement : Intent
}
data class State(val counter: Int)
sealed interface SideEffect {
data class CounterChanged(val value: Int) : SideEffect
data object Cleanup : SideEffect
}
}
Generated Handlers
The annotation processor generates factory functions for creating handlers:
// counterStoreIntentHandler is a generated function
fun incrementIntentHandler() = counterStoreIntentHandler<CounterStore.Intent.Increment> { intent ->
reduce { copy(counter = counter + 1) }
sideEffect(CounterStore.SideEffect.CounterChanged(counter = state.counter))
}
fun decrementIntentHandler() = counterStoreIntentHandler<CounterStore.Intent.Decrement> { intent ->
reduce { copy(counter = counter - 1) }
sideEffect(CounterStore.SideEffect.CounterChanged(counter = state.counter))
}
Init Handler:
// counterStoreInitHandler is a generated function
fun myInitHandler() = counterStoreInitHandler {
reduce { copy(initialized = true) }
// Initialization logic
}
Destroy Handler:
// counterStoreDestroyHandler is a generated function
fun myDestroyHandler() = counterStoreDestroyHandler {
sideEffect(CounterStore.SideEffect.Cleanup)
// Cleanup logic
}
Benefits:
- Type-safe intent handler creation
- Type-safe lifecycle handler creation
- Less boilerplate code
- Better code organization
- Compile-time safety
Setup:
Add the code generation dependency to your build file:
// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "<version>"
}
dependencies {
implementation("io.github.arttttt.simplemvi:simplemvi:<version>")
implementation("io.github.arttttt.simplemvi:simplemvi-annotations:<version>")
ksp("io.github.arttttt.simplemvi:simplemvi-codegen:<version>")
}
ActorScope
All actor implementations provide access to ActorScope, which offers:
interface ActorScope<in Intent : Any, State : Any, in SideEffect : Any> {
val state: State // Current state
val scope: CoroutineScope // Coroutine scope for async work
fun reduce(block: State.() -> State) // Update state
fun sideEffect(sideEffect: SideEffect) // Emit side effect
fun intent(intent: Intent) // Dispatch new intent
}
For detailed information about ActorScope, see the ActorScope documentation.
Example: Async Operations
Async operations in actors:
class DataActor : DefaultActor<DataIntent, DataState, DataSideEffect>() {
override fun handleIntent(intent: DataIntent) {
when (intent) {
is DataIntent.LoadData -> {
reduce { copy(loading = true) }
scope.launch {
try {
val data = repository.fetchData()
reduce { copy(loading = false, data = data) }
sideEffect(DataSideEffect.DataLoadSucceeded)
} catch (e: Exception) {
reduce { copy(loading = false, error = e.message) }
sideEffect(DataSideEffect.DataLoadFailed(e.message ?: "Unknown error"))
}
}
}
}
}
}
Choosing the Right Approach
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| DefaultActor | Complex logic, large projects | OOP structure, shared helpers | More boilerplate |
| actorDsl | Simple logic, quick development | Concise, declarative | Less structure for complex cases |
| delegatedActor | Modular composition, code generation | Flexible handlers | More verbose without codegen |
Important Notes
- The
Actoris tightly coupled with theStoreand should not be used independently. - Implementations of
Actorshould handle all business logic, state updates, and side effect production based on received intents. - All coroutines launched in the actor's scope are automatically cancelled when the store is destroyed.
- State updates via
reduceshould be pure functions without side effects.
For more information on how Actor interacts with other components, refer to the Store and ActorScope documentation.