I need to get user auth state in MVVM. In the repository I do this:
override fun getAuthResponse() = callbackFlow {
val listener = AuthStateListener {
Log.d(TAG, "currentUser: " (currentUser == null)) //Line2
trySend(it.currentUser == null)
}
auth.addAuthStateListener(listener)
awaitClose {
auth.removeAuthStateListener(listener)
}
}
"Line2" will always print true
because the user is not authenticated. Then in ViewModel I have:
fun getAuthResponse() = repo.getAuthResponse()
And inside activity:
setContent {
//...
val response = viewModel.getAuthResponse().collectAsState(initial = false).value
Log.d(TAG, "response: $response") //Line1
}
Since setContent is a composable function, when I open the app, it fires twice. That means that the log statement at "Line1", is triggered twice. When it first fires, I get:
response: false
currentUser: true
So, the response is printed before, even if I called getAuthResponse() at a previous line. The problem is that for some kind of reason even if the current user is null, I got printed in the activity that the user is not null. When it fires the second time, I got the correct data:
response: true
currentUser: true
Why do I get a non-null user object? Does trySend emit fake data?
CodePudding user response:
The problem here is that flows are by design asynchronous. Even if you expect AuthStateListener
to be invoked immediately after registering it, still the flow initially don't have any value. collectAsState()
requires a value, it can't be just empty and for this reason it requires you to provide an initial value. You provided false
and this is what you initially get as the response
. Then almost immediately listener is invoked, it emits true
and then return
changes to true
as well.
There are multiple ways how we could solve this kind of problems. For example, we could initially show a loading screen until we get the correct value. In the above case, as we are able to get the value for currentUser
directly, without using a listener, I suggest using StateFlow
instead. StateFlow
is different than a regular Flow
as it is able to keep its "current" value. The easiest would be to use MutableStateFlow
:
override fun getAuthResponse(): StateFlow<Boolean> {
val flow = MutableStateFlow(auth.currentUser == null)
val listener = AuthStateListener {
Log.d(TAG, "currentUser: " (currentUser == null)) //Line2
flow.value = it.currentUser == null
}
auth.addAuthStateListener(listener)
return flow
}
setContent {
//...
val response = viewModel.getAuthResponse().collectAsState().value
Log.d(TAG, "response: $response") //Line1
}
This solution may be not 100% correct as I couldn't test it, but I hope you get the idea.