The "trigger" pattern
Reactive programming and the observable pattern are everywhere nowadays. It is so omnipresent that it's hard to find a library which does not use it or enforce it.
It is even more so in the Android ecosystem where Google first released their LiveData
library in 2017 and is now more and more supporting Kotlin's Flow in most of their libraries.
In this article, I would like to shed some light on a pattern which I first encountered in Google's GitHub Browser Sample which I call the trigger pattern. It's been mentioned here and there on the web but without a proper explanation.
What is the Trigger pattern?
The idea is to use an observable
source to trigger a chain of calls which will result in the state you want your observer to have.
The Trigger pattern takes advantage of the Observable pattern, it allows you to chain operations to create the state of your observable
s
Here is an example with Flow
s:
interface Repository {
suspend fun retrieveSomeInfoForId(id: String): Result<Info>
}
private val trigger = MutableSharedFlow<String>(replay = 1)
val state: Flow<Result<Info>> = trigger.mapLatest { id ->
repository.retrieveSomeInfoForId(id)
}
You can see a real example in Google's GitHub Browser Sample's ViewModel
So what does it do exactly?
Everytime you push something inside trigger
(E.g.: trigger.emit("123")
) this will execute repository.retrieveSomeInfoForId
and each values emitted by the Flow
returned by retrieveSomeInfoForId
will be emitted by state
as well.
How is that useful?
Let's take an example of something you might have seen in some code bases where we have a screen that display some information about an id
it received as a parameter:
interface Repository {
suspend fun retrieveSomeInfoForId(id: String): Result<Info>
}
private val _info = MutableSharedFlow<Result<Info>>(replay = 1)
val info: Flow<Result<Info>> = _info
fun loadInfo(id: String) = viewModelScope.launch {
_info.emit(repository.retrieveSomeInfoForId(id))
}
And now, consider what would happen if you call loadInfo
twice, each time with a different id
. What if the second call finishes quicker than the first one?
And what if you want to do something with the info and push the result in a different Flow
?
interface Repository {
suspend fun retrieveSomeInfoForId(id: String): Result<Info>
suspend fun getSimilarNames(info: Info): List<String>
}
private val _info = MutableSharedFlow<Result<Info>>(replay = 1)
val info: Flow<Result<Info>> = _info
private val _similarNames = MutableSharedFlow<List<String>>(replay = 1)
val similarNames: Flow<List<String>> = _similarNames
fun loadInfo(id: String) = viewModelScope.launch {
val result = repository.retrieveSomeInfoForId(id)
_info.emit(result)
_similarNames.emit(emptyList())
result.onSuccess {
_similarNames.emit(repository.getSimilarNames(it))
}
}
Again, if you call loadInfo
twice, you could get into some inconsistent states and now the logic of fetching similar names is entangled with the logic of retrieving the Info
.
How could we make this code better?
With the trigger pattern, of course! Let's rewrite it and see how this would look like:
interface Repository {
suspend fun retrieveSomeInfoForId(id: String): Result<Info>
suspend fun getSimilarNames(info: Info): List<String>
}
private val id = MutableSharedFlow<String>(replay = 1)
val info: Flow<Result<Info>> = id.mapLatest { id ->
repository.retrieveSomeInfoForId(id)
}
val similarNames: Flow<List<String>> = info.transformLatest { result ->
emit(emptyList())
result.onSuccess {
emit(repository.getSimilarNames(it))
}
}
fun loadInfo(id: String) = viewModelScope.launch {
this.id.emit(id)
}
Let's look a little bit closer at what happens here. We call loadInfo
which launches a coroutine to emit into the trigger (the MutableSharedFlow
called id
) the id of the resource to load.
This, in turns, launches the transformation of info
to retrieve the information we are looking for.
When info
emits, it will also launch the transformation of similarNames
.
As you can see, the code is not all cramped up into one method anymore. Also, each new emit
will cancel the previous transformations, without the need for us to keep a reference to them and canceling them when receiving a new id
, thanks to the mapLatest
and transformLatest
methods. This makes this less error prone.
After thoughts
New subscribers and resubscribes
This post is focusing on the concept of emitting a value into an observable
which then 'triggers' some logic into other observable
s. Yet you may have noticed that those observable
s would execute their transformation multiple times when some new subscriber subscribed. It's often not what we want. In that case, it's a good idea to use shareIn and stateIn but I will not go into details about those as there are already plenty of good articles about SharedFlow
s and StateFlow
s.
emit
and the execution order
It is important to note that using this method, calling loadInfo(id: String)
another time won't cancel the previous unfinished coroutines (the lambda in viewModelScope.launch
, I.e. the call to emit
). Therefore, the order of the calls to emit
into the trigger is important. When using viewModelScope.launch
, you are actually using the Dispatchers.Main.immediate
so the coroutines will be queued and it will not be a problem, unless you suspend before the call to emit
. This problem is also present with the code we have improved but it is far less likely to append with the trigger pattern as we tend to not have any logic into the "triggering" methods.