DEV Community

Cover image for Using the event bus pattern in Android with Kotlin
Tristan Elliott
Tristan Elliott

Posted on

Using the event bus pattern in Android with Kotlin

Table of contents

  1. Event bus patter
  2. SharedFlow
  3. Extending the event bus pattern

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:

Event bus pattern

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)

}
}

Enter fullscreen mode Exit fullscreen mode
  • 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. if viewModelScope 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
                )
            }
        }
    }

Enter fullscreen mode Exit fullscreen mode
  • 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,
)

Enter fullscreen mode Exit fullscreen mode
  • 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)
    )

Enter fullscreen mode Exit fullscreen mode
  • 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 by combine() 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
                }
            }
        }
    }

Enter fullscreen mode Exit fullscreen mode
  • 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
                    )
                )

Enter fullscreen mode Exit fullscreen mode
  • 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)