Table of contents
The code
Introduction
- Recently I have had to deal with some relatively complex state logic for a login system and this was how I dealt with it.
Event bus pattern
So in nerd talk we can describe the Event bus pattern as this:
Event-driven architecture pattern is a distributed asynchronous architecture pattern to create highly scalable reactive applications. The pattern suits for every level application stack from small to complex ones. The main idea is delivering and processing events asynchronously.
Which basically means we can have an event producer object and event subscriber objects that wait and listen for the event producer to produce events. The subscribers can then consume and respond to those events. The classic mental model is down below:
SharedFlow
- Now if you are unfamiliar with what a SharedFlow is, just know that it is a hot flow. Which means, that when the terminal operator
collect{}
is called it will always be active. - However, we are going to use a specialized version of a SharedFlow called the StateFlow for our event bus:
class DataStoreViewModel(): ViewModel() {
private val _oAuthUserToken:MutableStateFlow<String?> = MutableStateFlow(null)
init{
viewModelScope.launch{
_oAuthUserToken.collect{ token ->
token?.let{oAuthToken ->
// make request now that the oAuthToken is not null
}
}
}
}
fun setOAuthToken(token:String){
_oAuthUserToken.tryEmit(token)
}
}
- In the code block above we are
registering a subscriber
when calling_oAuthUserToken.collect{}
. Meaning that it will always be active as long as the scope it is calling is active. ifviewModelScope
is destroyed then so is the subscriber.
Extending the event bus pattern
- Now there are going to be times when your authentication process has multiple steps and each step relies on the previous one. Also, at anytime, one of those steps can fail meaning that the steps that relied on that one also fail. So how do we model this problem? We extend the event bus pattern like so:
data class MainBusState(
val oAuthToken:String? = null,
val authUser:ValidatedUser? = null,
)
private val authenticatedUserFlow = combine(
flow =MutableStateFlow<String?>(null),
flow2 =MutableStateFlow<ValidatedUser?>(null)
){
oAuthToken,validatedUser ->
MainBusState(oAuthToken,validatedUser)
}.stateIn(viewModelScope, SharingStarted.Lazily,
MainBusState(oAuthToken = null, authUser = null)
)
private val mutableAuthenticatedUserFlow = MutableStateFlow(authenticatedUserFlow.value)
private fun collectAuthenticatedUserFlow() =viewModelScope.launch {
mutableAuthenticatedUserFlow.collect{mainState ->
mainState.oAuthToken?.let{notNullToken ->
validateOAuthToken(notNullToken)
}
mainState.authUser?.let {user ->
_uiState.value = _uiState.value.copy(
clientId = user.clientId,
userId = user.userId
)
}
}
}
-
The above code can be broken down into 4 steps:
1) model your main state
2) combine your flows
3) register the subscribers
4) emit to the flow
1) Model your main state
data class MainBusState(
val oAuthToken:String? = null,
val authUser:ValidatedUser? = null,
)
- each value in the MainBusState represents a step inside of the authentication process. With the code you can tell that my app needs an oAuth Authentication token and it needs to validate the user. The validated user step can not happen if there is no oAuth Authentication token
2) Combine your flows
private val authenticatedUserFlow = combine(
flow =MutableStateFlow<String?>(null),
flow2 =MutableStateFlow<ValidatedUser?>(null)
){
oAuthToken,validatedUser ->
MainBusState(oAuthToken,validatedUser)
}.stateIn(viewModelScope, SharingStarted.Lazily,
MainBusState(oAuthToken = null, authUser = null)
)
- This is done so we can take multiple flows and combined them into one object. This makes it much easier and more readable, we simply have to subscribe to a single flow. The
stateIn()
is just us turning the cold flow produced bycombine()
into a hot flow
3) register the subscribers
private fun collectAuthenticatedUserFlow() =viewModelScope.launch {
mutableAuthenticatedUserFlow.collect{mainState ->
mainState.oAuthToken?.let{notNullToken ->
validateOAuthToken(notNullToken)
}
mainState.authUser?.let {user ->
_uiState.value = _uiState.value.copy(
clientId = user.clientId,
userId = user.userId
}
}
}
}
- When this function is run, it will last as long as the viewModelScope and it registers 2 subscribers. One subscriber is listening to the oAuthToken and the other to the authUser. Both of which have their respective functions to run when they are not null.
4) Emit to the flow
mutableAuthenticatedUserFlow.tryEmit(
mutableAuthenticatedUserFlow.value.copy(
oAuthToken = newTokenStrin
)
)
- Then to update the value to our mutableAuthenticatedUserFlow we simple call tryEmit() with the updated values of our MainBusState. The new emitted value will be given to our mutableAuthenticatedUserFlow and the values that are updated will given to their respective listeners.
Conclusion
- Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
Top comments (0)