ActorScope
Overview
ActorScope is the execution context provided to all actors (both DefaultActor, actorDsl, and delegatedActor). It provides access to state, coroutine management, and state modification capabilities.
Interface Definition
public interface ActorScope<in Intent : Any, State : Any, in SideEffect : Any> {
val state: State
val scope: CoroutineScope
fun intent(intent: Intent)
fun reduce(block: State.() -> State)
fun sideEffect(sideEffect: SideEffect)
}
Properties
state
Type: State (read-only)
Provides access to the current state of the store:
onIntent<MyIntent.Calculate> { intent ->
val currentValue = state.value // Read current state
val newValue = currentValue * 2
reduce { copy(value = newValue) }
}
Important characteristics:
- This property always returns the latest state, even after multiple
reducecalls within the same intent handler - The state is read-only; modifications must be done through
reduce - Accessing state is synchronous and safe
Example - Using state for conditional logic:
onIntent<MyIntent.Decrement> { intent ->
if (state.counter > 0) {
reduce { copy(counter = counter - 1) }
} else {
sideEffect(MySideEffect.CounterMinimumReached)
}
}
scope
Type: CoroutineScope
The coroutine scope tied to the store's lifecycle:
onIntent<MyIntent.LoadData> { intent ->
reduce { copy(loading = true) }
scope.launch {
try {
val data = repository.fetchData()
reduce { copy(loading = false, data = data) }
} catch (e: Exception) {
reduce { copy(loading = false, error = e.message) }
}
}
}
Important characteristics:
- All coroutines launched in this scope are automatically cancelled when the store is destroyed
- The scope uses the
CoroutineContextprovided during store creation - By default, uses
Dispatchers.Main.immediate
Example - Multiple async operations:
onIntent<MyIntent.LoadProfile> { intent ->
reduce { copy(loading = true) }
scope.launch {
try {
// Launch parallel requests
val userDeferred = async { userRepository.getUser(intent.userId) }
val postsDeferred = async { postsRepository.getPosts(intent.userId) }
val user = userDeferred.await()
val posts = postsDeferred.await()
reduce {
copy(
loading = false,
user = user,
posts = posts
)
}
sideEffect(MySideEffect.ProfileLoadSucceeded)
} catch (e: Exception) {
reduce { copy(loading = false, error = e.message) }
sideEffect(MySideEffect.ProfileLoadFailed(e.message ?: "Unknown error"))
}
}
}
Functions
reduce
Updates the store's state. The lambda receives the current state and must return a new state:
// Simple state update
reduce { copy(counter = counter + 1) }
// Multiple property updates
reduce {
copy(
counter = counter + 1,
lastUpdate = System.currentTimeMillis(),
isModified = true
)
}
// Conditional updates
reduce {
if (counter > 0) {
copy(counter = counter - 1)
} else {
this // Return unchanged state
}
}
Important rules:
- State must be immutable (use
data classwithvalproperties) - Each
reducecall creates a new state instance - Middleware is notified of every state change
- The lambda should be a pure function without side effects
Example - Complex state transformation:
onIntent<MyIntent.UpdateUser> { intent ->
reduce {
copy(
user = user.copy(
name = intent.newName,
email = intent.newEmail,
updatedAt = Clock.System.now()
),
isDirty = true
)
}
}
Anti-pattern - Side effects in reduce:
// ❌ BAD - Don't do this
reduce {
logEvent("state_changed") // Side effect!
copy(value = value + 1)
}
// ✅ GOOD - Keep reduce pure
reduce { copy(value = value + 1) }
logEvent("state_changed")
sideEffect
Emits a side effect from the actor. Side effects represent one-time events that cannot be represented in state:
onIntent<MyIntent.SaveData> { intent ->
reduce { copy(saving = true) }
scope.launch {
val result = repository.save(intent.data)
if (result.isSuccess) {
reduce { copy(saving = false) }
sideEffect(DataSideEffect.DataSaveSucceeded)
} else {
reduce { copy(saving = false, error = result.error) }
sideEffect(DataSideEffect.DataSaveFailed(result.error))
}
}
}
Example side effects:
sealed interface DataSideEffect {
data object DataSaveSucceeded : DataSideEffect
data class DataSaveFailed(val reason: String) : DataSideEffect
data class ValidationFailed(val errors: List<String>) : DataSideEffect
data object SessionExpired : DataSideEffect
}
intent
Dispatches a new intent to be processed by the actor. Useful for creating multi-step workflows:
onIntent<MyIntent.StartProcess> { intent ->
reduce { copy(step = 1, status = "Processing step 1") }
// Process step 1...
// Move to next step
intent(MyIntent.ProcessStep2)
}
onIntent<MyIntent.ProcessStep2> { intent ->
reduce { copy(step = 2, status = "Processing step 2") }
// Process step 2...
intent(MyIntent.ProcessStep3)
}
onIntent<MyIntent.ProcessStep3> { intent ->
reduce { copy(step = 3, status = "Process complete") }
sideEffect(MySideEffect.ProcessCompleted)
}
Use cases:
- Splitting complex logic into multiple steps
- Creating intent chains for sequential operations
- Conditional workflows based on state or results
Example - Conditional intent dispatching:
onIntent<MyIntent.CheckAndLoad> { intent ->
if (state.isAuthenticated) {
intent(MyIntent.LoadUserData)
} else {
sideEffect(MySideEffect.AuthenticationRequired)
}
}
Warning: Be careful not to create infinite loops:
// ❌ BAD - Infinite loop!
onIntent<MyIntent.BadIntent> { intent ->
intent(MyIntent.BadIntent) // Calls itself!
}
// ✅ GOOD - Use conditions to break loops
onIntent<MyIntent.Retry> { intent ->
if (state.retryCount < 3) {
reduce { copy(retryCount = retryCount + 1) }
scope.launch {
// Retry logic
}
} else {
sideEffect(MySideEffect.MaxRetriesReached)
}
}