Android Quick Recipes: Execute your UseCases automatically when the users logs in/out with Flow
Does you app have a login? Then I bet that you have a conditional logic that has to be executed when the users logs in or maybe when the user logs out or probably both. Well, by using the power of Flow we are gonna see a quick and robust way to have all that handled in a very elegant (well, that’s my opinion) way.
Defining our scenario
We are gonna use MVVM, this shouldn’t be a problem for other approaches, and that means a ViewModel which will have use cases to keep our business logic isolated and reusable. Use cases mission is also to talk with our repository and orchestrate several of them if needed.
Our goal is to have some use cases executed every time the user logs in without having to manually trigger them and unit test that logic.
This scenario in bullet points
- MVVM + unidirectional data flow
- Use cases to get user information are executed only when the user is logged in and cancelled if it logs out
- ViewModel only exposes one Flow with the UiState model. This UiState represents the state of the current UI and has all the info needed to paint the UI
- Unit test
The sample app has a couple of button to simulate login/logout and present some user related info when the user is logged in. It looks like this:
Show me the code
Before jumping to the coordination of the use cases with Flow, we will check the setup of the project and the classes involved. Feel free to jump to the solution in the next section.
Let’s take a look to our principal use case for the scenario. To get if a user is logged in or not, we’ll have a GetLoggedUserUseCase which will expose a Flow<User?>. Whenever there is a user logged in, Flow will emit a User, when nobody is at home, will emit null. I
/**
* Return user information or null if the user is not logged in
*/
class GetLoggedUserUseCase @Inject constructor(private val userRepository: UserRepository) {
operator fun invoke() : Flow<User?> = userRepository.loggedInUser
}
For this sample project we have created a fake UserRepository which exposes the Flow we need, but you can check this other article for a way to get a Flow from the AccountManager (the place where usually the user accounts are stored in android) which emits when the user is logged in or not. Let’s check our convenient UserRepository.
/**
* A repository simulating login and exposing a user flow. Stores a flow internally for simplicity, but the source of truth should be
* the account manager
*/
class UserRepositoryImp @Inject constructor(): UserRepository {
override suspend fun login() {
val fakeUser = User("a123", "Finn the Human")
_loggedInUser.emit(fakeUser)
}
override suspend fun logout() {
_loggedInUser.emit(null)
}
private val _loggedInUser = MutableStateFlow<User?>(null)
/**
* Exposes the currently logged [User] if any or null if there isn't any
*/
override val loggedInUser: Flow<User?> = _loggedInUser
}
We’ll have two extra use cases which return some user related data and that are supposed to be executed only when the user is logged in. These use cases return a Flow of data when executed.
/**
* Fake UseCase which returns the user Address. The real use case would use a repository which
* might trigger a request or get the data from Db. For this example, this works.
*/
class GetUserAddressUseCase @Inject constructor() {
operator fun invoke(): Flow<Address> {
return flowOf(Address("Prime Street 19", "00019"))
}
}class GetUserBadgesUseCase @Inject constructor() {
operator fun invoke(): Flow<List<Badge>> {
return flowOf(listOf(Badge("gold"), Badge("silver")))
}
}
The initial look of our ViewModel, before start playing with flow, it’s the following:
class MainViewModel @Inject constructor(
getLoggedUserUseCase: GetLoggedUserUseCase,
private val getUserAddressUseCase: GetUserAddressUseCase,
private val getUserBadgesUseCase: GetUserBadgesUseCase,
private val doLoginUseCase: DoLoginUseCase,
private val doLogoutUseCase: DoLogoutUseCase
): ViewModel() {
val uiState = flowOf(UiState.Empty) // TODO our logic here
fun onLoginClicked() {
viewModelScope.launch {
doLoginUseCase()
}
}
fun onLogoutClicked() {
viewModelScope.launch {
doLogoutUseCase()
}
}
}
The last part to check is the UiState class, which represents the state of MainActivity in a particular point in time:
sealed class UiState {
object Empty : UiState()
data class LoggedIn (
val userBadges: List<Badge>,
val userAddress: Address
) : UiState()
object LoggedOut : UiState()
}
We won’t take a look to the UI but the git repository is there at the bottom in case you wanna check the details.
Execute use cases only when the user logs in
We want our UI to change whenever the user state (logged in or logged out) changes. To start we will plug getLoggedUserUseCase() to the uiState Flow and will add some Flow operators.
val uiState = getLoggedUserUseCase()
.map { user -> user != null }
.distinctUntilChanged()
.flatMapLatest { isLoggedIn ->
// TODO execute use cases when the user is logged in
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState.Empty)
- getLoggedUseCase() returns a Flow which will emit null if the user is not logged in and a User object when is logged in
- map{ user -> user != null} will transform the Flow to one that emits true or false depending if the user is logged in or not.
- distinctUntilChanged() will skip repeated values, so collection will only happen when the user moves from logged in to logged out and the other way around
- flatMapLatest() will cancel any previous suspend function which was being executed when a new value is emitted. Imagine that whatever use case we execute inside flatMapLatest() takes some time to complete, and the user logs out during that time. With this operator we ensure that the use case is cancelled, so we don’t waste resources (or worse, download something we are not supposed to)
Let’s now finalize the code by executing our two use cases only when the user is logged in. The output of this two use cases is combined to generate the UiState with the required data.
val uiState = getLoggedUserUseCase()
.map { user -> user != null }
.distinctUntilChanged()
.flatMapLatest { isLoggedIn ->
if (isLoggedIn) {
combine(getUserBadgesUseCase(), getUserAddressUseCase()){ badges, address ->
UiState.LoggedIn(badges, address)
}
} else {
flowOf(UiState.LoggedOut)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState.Empty)
When uiState flow gets collected, our cascade of operators will start executing. When the user is logged in, we are executing both use cases and combining them with combine operator.
Combine will run the lambda when both use cases has emitted something, which ends up returning a UiState data class which has been built with the data coming from the use cases.
Imagine that login is triggered from any other screen of your app. While the user is logged out, the uiState will emit LoggedOut, but the moment the user logs in, our flatMapLatest() will execute again emitting true and executing the use cases, creating a new UiState and updating the UI. Now let’s say that our user can be logged out from other part of the app, like if the server returns a 401 (commonly associated to a user not being authenticated) and you have logic in place to delete your user data in that case. This screen will change from LoggedIn state to LoggedOut, and if the use cases where still being executed those will be cancelled, all this automagically.
Bonus: loading and error state
To keep things simple, I’ve excluded error and loading from the sample, but both getUserBadgesUseCase and getUserAddressUseCase are likely to get data from network thus having the user waiting or, in the worst case, having an error. We could solve it by adding UiState.Loading and UiState.Error to our UiState sealed class and handling those states like the following:
combine(getUserBadgesUseCase(), getUserAddressUseCase()){ badges, address ->
UiState.LoggedIn( userBadges = badges, userAddress = address)
}.onStart { UiState.Loading }
.catch { UiState.Error()}
Now thanks to onStart the first value emitted will be UiState.Loading followed by the result from executing both use cases. If anything goes south, catch will take care of emitting an UiState.Error, cool.
Unit testing
Well, we mentioned unit testing so let’s take a look to a couple of test for each scenario. Please go to the repository for a more complete suit of test.
@Test
fun `when user is logged then getAddressUseCase is executed`() = runTest {
every { getLoggedUserUseCase() } returns flowOf(User("", ""))
val tested = createViewModel()
tested.uiState.take(2).last()
verify(exactly = 1) { getUserAddressUseCase() }
}
The test above will check that we are indeed executing one of the use cases when the user is logged in. The line uiState.take(2) will get the first two emitted values, first will be UiState.Empty and second UiState.LoggedIn. For this test, we only check with verify that the use case was executed.
Our second test will do something more complicated and check that the use case are actually cancelled when the user is first logged in and later logged out.
@Test
fun `when user goes from logged in to not logged in then use cases are cancelled`() = runTest {
every { getLoggedUserUseCase() } returns flow {
emit(User("", ""))
delay(50)
emit(null)
}
// add some delay to usecases so we can cancel them
every { getUserAddressUseCase() } returns flow {
delay(100)
emit(Address("street", "0"))
}
every { getUserBadgesUseCase() } returns flow {
delay(100)
emit(listOf())
}
val tested = createViewModel()
val emittedStates = tested.uiState.take(2).toList()
// If last UI state is LoggedOut, then the useCases where not finished
assertEquals(UiState.Empty, emittedStates[0])
assertEquals(UiState.LoggedOut, emittedStates[1])
}
Now getLoggedUserUseCase has a delay before emitting that the user is no logged in. The other two getUserAddressUseCase and getUserBadgesUseCase also have some delays, so those should be cancelled when getLoggedUserUseCase emits null after 50 millis. We cannot use verify because the use case is always executed, we need to know if it was cancelled. For that we’ll take the first two emitted elements, if the use cases were executed, then the second element should be UiState.LoggedIn, but if it’s UiState.LoggedOut that means that they were cancelled on time.
There are some other test in the repo.
Final thoughts
Use cases are a great way to group your business logic and we’ve seen a tool to coordinate them in a common situation that is a user logged in and out without you having to call manually any method from the UI. It also avoids using a mutable Flow, always great to avoid emitting from multiple places inside the ViewModel. Finally, we have unit tested our logic, because testing is good ;). I hope that you find it useful, thanks for reaching to the end and happy coding!