Blog Infos
Author
Published
Topics
, ,
Author
Published
Posted by: Abhi Muktheeswarar

There are already so many app-level architecture and presentation layer patterns (MVC/MVP/MVVM/MVI/MVwhatever) that exist to help us manage the complexity and state of our application without going insane. Each pattern has its pros and cons.

Here, the state could be anything, it could represent the whole app, a screen, a view component, a shopping cart, a user’s authentication state, or anything. It is up to you to define the scope of the state.

How you define the state is an art in itself.

Everything in our application happens concurrently. We have network requests, database operations, system broadcasts, push notifications, lifecycle events, and user inputs all going on concurrently.

The concept of threading is far more complex than it looks. Let’s take a simple counter-example that simulates a concurrent environment where multiple threads are trying to update a mutable state without using any synchronization mechanism or concurrent utilities.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
     var counter = 0
   
    withContext(Dispatchers.Default) {
        massiveRun {
            counter++
        }
    }
   
    println("Counter = ${counter}")
}

//Helper function to simulate a massive concurrent input of messages
suspend fun massiveRun(action: suspend () -> Unit) {
    val n = 100  // number of coroutines to launch
    val k = 1000 // times an action is repeated by each coroutine
    val time = measureTimeMillis {
        coroutineScope { // scope for coroutines 
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("Completed ${n * k} actions in $time ms")    
}

The final value of the counter should be 100000, but it hardly ever is, unless you get very lucky.

What if there is a way to access and update the state safely across threads without explicitly defining any synchronization mechanism or using concurrent utilities?

But why?

Handling the state in concurrent systems is hard. We all know and might have learned the hard way that concurrent read and write to a shared mutable state will always put our app in a non-deterministic (unexpected) state if not set up properly to deal with the concurrent nature of the environment our application lives in. We can’t avoid it, so we usually end up using some sort of synchronization mechanism as a solution, most of the time as an afterthought.

The significance of having proper state management becomes much more apparent when working on a Kotlin-MultiPlatform (KMP) project since KMP doesn’t provide any synchronization mechanisms to deal with concurrency out of the box.

"Synchronization on any object is not supported on every platform and will be removed from the common standard library soon." - Kotlin

Additionally, Kotlin has two rules regarding state sharing in native platforms (non JVM).

  • Rule 1: Mutable state == 1 thread. If your state is mutable, only one thread can see it at a time.
  • Rule 2: Immutable state == many threads. If a state can’t be changed, multiple threads can safely access it.

These two rules can be considered as a good practice for all platforms .

So to handle concurrency that also satisfies the above rules, either we need to use some libraries providing concurrent utilities or provide our own platform-specific synchronization mechanism.

Enter Actor(s)

The Actor model is a conceptual model to deal with concurrent computation. It defines a set of guidelines on how system components should interact in a concurrent computational environment.

In a typical concurrency model, we use synchronization primitives to prevent starvation, deadlocks, and race conditions when multiple threads try to access or mutate a shared state. So, basically, we are sharing the memory (state) between multiple threads which leads to all sorts of problems when we scale.

Actor(s) on the other hand is based on the idea of

Do not communicate by sharing memory; instead share memory by communicating.

What is Actor?

An Actor in an Actor model is the fundamental unit of computation. They encapsulate state, and a message queue (like a mailbox). Messages are sent asynchronously to an Actor and it will process the received messages sequentially in FIFO order.

The most famous implementations of the Actor Model are Akka and Erlang. Even though Actors are predominantly used in the backend systems, we can adapt some of its traits for the frontend applications to help us with managing the state efficiently to achieve a deterministic state behaviour.

How does Actor help?

An Actor runs in its own thread and its private state can be accessed or modified only by processing the received messages from its queue (mailbox). To even access the state of an Actor, you send a message requesting the current state and the Actor will provide a copy of its state.

Messages are simple, immutable data structures that can be sent to an Actor from any thread.

This removes the need for lock-based synchronization, because:

The Actor model is designed to be message-driven and non-blocking. It has synchronization built into it by its design.

This also satisfies the above-mentioned rules of concurrency in Kotlin Native and allows us to define the state management implementation in the common module itself, thus removing the need to provide platform-specific implementations.

Actor test drive

Kotlin along with coroutines helps us build an Actor based system quite easily. In fact, Kotlin coroutines come with experimental actorimplementation.

To begin with, we will use the experimental actor coroutine builder that comes with a mailbox Channel to receive messages from outside.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlin.system.*

// Message types for counterStateMachine
sealed interface CounterMessage
object IncrementCounter : CounterMessage // one-way message to increment counter
class GetCounter(val response: CompletableDeferred<Int>) : CounterMessage // a request with reply

fun CoroutineScope.counterStateMachine() = actor<CounterMessage> {
    var counter = 0 // actor state
    channel.consumeEach { message ->
        when (message) {
            is IncrementCounter -> counter++
            is GetCounter -> message.response.complete(counter)
        }
    }
}

fun main() = runBlocking<Unit> {
    val counterStateMachine = counterStateMachine() // create the counterStateMachine actor
    withContext(Dispatchers.Default) {
        massiveRun {
            counterStateMachine.send(IncrementCounter)
        }
    }
    // send a message to get the counter value from the counterStateMachine actor
    val response = CompletableDeferred<Int>()
    counterStateMachine.send(GetCounter(response))
    val count = response.await()
    println("Counter = ${count}")
    counterStateMachine.close() // shutdown the counterStateMachine actor
}

//Helper function to simulate a massive concurrent input of messages
suspend fun massiveRun(action: suspend () -> Unit) {
    val n = 100  // number of coroutines to launch
    val k = 1000 // times an action is repeated by each coroutine
    val time = measureTimeMillis {
        coroutineScope { // scope for coroutines 
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("Completed ${n * k} actions in $time ms")    
}

You can execute the above code and witness the power of the Actor model yourself!

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

Building a simple counter app

Let’s see how we can use the Actor model concept to manage our counter app state.

This example is based on MVI/Flux/Redux, simply put, uni-directional flow concept.

First, define the state:

data class CounterState(val count: Int = 0)

Define messages that our Actor based state machine can process:

sealed interface CounterMessage
object IncrementCounter : CounterMessage
object DecrementCounter : CounterMessage
class GetCounterState(val deferred: CompletableDeferred<CounterState>) : CounterMessage

Our Actor based state machine and Store:

fun CoroutineScope.counterStateMachine(
initialState: CounterState,
mutableStateFlow: MutableStateFlow<CounterState>,
mutableMessages: MutableSharedFlow<CounterMessage>,
) =
actor<CounterMessage> {
var state: CounterState = initialState
channel.consumeEach { message ->
when (message) {
is IncrementCounter -> {
state = state.copy(count = state.count + 1)
mutableStateFlow.emit(state)
mutableMessages.emit(message)
}
is DecrementCounter -> {
state = state.copy(count = state.count - 1)
mutableStateFlow.emit(state)
mutableMessages.emit(message)
}
is GetCounterState -> message.deferred.complete(state)
}
}
}
class CounterStateStore(initialState: CounterState, val scope: CoroutineScope) {
private val mutableStateFlow = MutableStateFlow<CounterState>(initialState)
private val mutableMessages = MutableSharedFlow<CounterMessage>()
private val stateMachine =
scope.counterStateMachine(initialState, mutableStateFlow, mutableMessages)
val stateFlow: Flow<CounterState> = mutableStateFlow
val messagesFlow: Flow<CounterMessage> = mutableMessages
fun dispatch(message: CounterMessage) {
stateMachine.trySend(message)
}
suspend fun getState(): CounterState {
val completableDeferred = CompletableDeferred<CounterState>()
dispatch(GetCounterState(completableDeferred))
return completableDeferred.await()
}
}

For the sake of simplicity, think of StateMachine like a container holding the state and the logic to update the state.

The CounterStateStore provides the inputs to run the counterStateMachine.

In the context of Android or any frontend application where the view layer needs to react to state changes, we slightly modified the Actor to broadcast the state changes as a StateFlow . Others can get the current state using GetCounterState message. The “others” here refer to side-effects and more.

SideEffects are the place where we do IO operations, navigation, logging, etc… Think of it as the use-case in the domain layer of clean architecture. We defined SideEffect as a separate class just for example, but anything can act as a SideEffect by listening to messages or state changes from the CounterStateStore . The SideEffect can also send messages to the CounterStateStore .

We can also define the SideEffect as an Actor, kind of a worker Actor for our main Actor: counterStateMachine.

class Repository {
suspend fun update(count: Int): Boolean {
TODO("Save to DB")
}
}
class CounterSideEffect(
private val repository: Repository,
private val counterStateStore: CounterStateStore,
) {
init {
counterStateStore.messagesFlow
.onEach(::handle)
.launchIn(counterStateStore.scope)
}
private fun handle(message: CounterMessage) {
when (message) {
IncrementCounter -> {
counterStateStore.scope.launch {
val currentState = counterStateStore.getState()
repository.update(currentState.count)
}
}
DecrementCounter -> TODO()
}
}
}

The CounterSideEffect listens for IncrementCounter message and write the counter value to our imaginary database.

The CounterViewModel acts as a lifecycle aware container for our CounterStateStore.

class CounterViewModel(private val counterStateStore: CounterStateStore) : ViewModel() {
val stateFlow: Flow<CounterState> = counterStateStore.stateFlow
fun dispatch(message: CounterMessage) {
counterStateStore.dispatch(message)
}
override fun onCleared() {
super.onCleared()
counterStateStore.scope.cancel()
}
companion object {
fun getViewModel(): CounterViewModel {
val scope = CoroutineScope(SupervisorJob())
val counterStateStore = CounterStateStore(CounterState(), scope)
val repository = Repository()
CounterSideEffect(repository, counterStateStore)
return CounterViewModel(counterStateStore)
}
}
}

Finally, the view:

class CounterActivity : AppCompatActivity() {
private val viewModel by viewModels<CounterViewModel>()
private lateinit var binding: ActivityCounterBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCounterBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.buttonDecrement.setOnClickListener {
viewModel.dispatch(DecrementCounter)
}
binding.buttonIncrement.setOnClickListener {
viewModel.dispatch(IncrementCounter)
}
lifecycleScope.launchWhenCreated {
viewModel.stateFlow.collect(::setupViews)
}
}
private fun setupViews(state: CounterState) {
binding.textCount.text = state.count.toString()
}
}

That’s it, we have an Actor model based state management without using any synchronization primitives.

Wrapping up

There’s a caveat in using Kotlin’s experimental actor . Kotlin has recently marked the actor with ObsoleteCoroutinesApi . Since the experimental actor implementation cannot be extended to suit complex use cases, Kotlin will be deprecating it to make way for more powerful Actors. It doesn’t mean, end of life. The good news is, we can build our own Actor.

As we mentioned earlier, an Actor for state management contains an internal state, a mailbox (Channel) to receive messages, and the business logic to decide how to change its state based on the received message. With that we can roll out our own Actor based state machine:

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlin.system.*

sealed interface CounterMessage
object IncrementCounter : CounterMessage // one-way message to increment counter
object DecrementCounter : CounterMessage // one-way message to increment counter
class GetCounter(val response: CompletableDeferred<Int>) : CounterMessage // a request with reply


fun CoroutineScope.customCounterStateMachine(channel : ReceiveChannel<CounterMessage>) = launch {
    var counter = 0 // actor state
    channel.consumeEach { message ->
        ensureActive()
        when (message) {
            is IncrementCounter -> counter++
            is GetCounter -> message.response.complete(counter)
        }
    }
}

fun main() = runBlocking<Unit> {
    val channel = Channel<CounterMessage>()
    val counterStateMachine = customCounterStateMachine(channel) // create the counterStateMachine actor
    withContext(Dispatchers.Default) {
        massiveRun {
            channel.send(IncrementCounter)
        }
    }
    // send a message to get the counter value from the counterStateMachine actor
    val response = CompletableDeferred<Int>()
    channel.send(GetCounter(response))
    val count = response.await()
    println("Counter = ${count}")
    channel.close() // shutdown the counterStateMachine actor
}

//Helper function to simulate a massive concurrent input of messages
suspend fun massiveRun(action: suspend () -> Unit) {
    val n = 100  // number of coroutines to launch
    val k = 1000 // times an action is repeated by each coroutine
    val time = measureTimeMillis {
        coroutineScope { // scope for coroutines 
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("Completed ${n * k} actions in $time ms")    
}

For a complete Actor based setup, you can check out the Flywheel library, which implements the Actor model concept based on a uni-directional flow concept covering all practical use cases.

 

GitHub – abhimuktheeswarar/Flywheel: A simple and predictable state management library inspired by…

A simple and predictable state management library inspired by Redux for Kotlin Multiplatform using the concepts of…

github.com

 

Generally, they say: one Actor is no Actor. Actors come in systems, an Actor can create more Actors (think of it as a worker Actor). It is true in the backend world, where generally multiple Actors will be running and communicating with each other through messages. You can relate it to (sort of) microservices architecture. Actors can be distributed across multiple systems (servers). As long as we know the address of the Actor, we can establish communication.

In the context of Android, to achieve better separation of concerns and modularize our codebase, we can design our features, components, repositories, use-cases, modules, dynamic modules as Actors and provide a common communication channel for all Actors to communicate with each other at the app level or any feature level. This might sound similar to an EventBus pattern. Yes, it is, albeit a much better and a powerful one.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
To get started, let’s see what ʻStructured Concurrency’ may look like in a real-world…
READ MORE
blog
With Compose Multiplatform 1.6, Jetbrains finally provides an official solution to declare string resources…
READ MORE
blog
This article will continue the story about the multi-platform implementation of the in-app review.…
READ MORE
blog
As a developer working on various Kotlin Multiplatform projects, whether for your job or…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu