The "trigger" pattern

Some cogs forming a mecanism
Zeitwerk Details I by Romain Guy: https://www.flickr.com/photos/romainguy/50785385016/

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 observables

Here is an example with Flows:

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 observables. Yet you may have noticed that those observables 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 SharedFlows and StateFlows.

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.

Subscribe to Android Dev Social

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe