I have these screens:
NavHost(
navController = navController,
startDestination = AuthScreen.route
) {
composable(
route = AuthScreen.route
) {
AuthScreen()
}
composable(
route = MainScreen.route
) {
MainScreen()
}
}
In the MainActivity, I need to send the user to the right screen according to the auth state. So I use:
val isUserSignedOut = viewModel.getAuthState().collectAsState().value
if (isUserSignedOut) {
navController.navigate(AuthScreen.route) {
popUpTo(navController.graph.id) {
inclusive = true
}
}
} else {
navController.navigate(MainScreen.route) {
popUpTo(navController.graph.id) {
inclusive = true
}
}
}
And everything works as expected, because I think that this is a synchronous operation. Now, when the user is authenticated, I need to create a request to check the type of user. Here is what I have tried:
if (isUserSignedOut) {
navController.navigate(AuthScreen.route) {
popUpTo(navController.graph.id) {
inclusive = true
}
}
} else {
Request(
checkAdmin = { admin ->
if (admin) {
navController.navigate(AdminScreen.route) {
popUpTo(navController.graph.id) {
inclusive = true
}
}
} else {
navController.navigate(MainScreen.route) {
popUpTo(navController.graph.id) {
inclusive = true
}
}
}
}
)
}
The problem with this code is that when I open the app, first time I get the AuthScreen
displayed, even if the user is authenticated. In other words, while I get the response of the asynchronous request, the AuthScreen
is displayed and I don't want that. I need go to the MainScreen
or AdminScreen
without displaying the AuthScreen
first. How to solve this?
Edit:
@BenjyTec I'm getting the auth state from the ViewModel:
fun getAuthState() = authRepo.getAuthState(viewModelScope)
And here is the repo:
override fun getAuthState(viewModelScope: CoroutineScope) = callbackFlow {
val listener = FirebaseAuth.AuthStateListener { auth ->
trySend(auth.currentUser == null)
}
auth.addAuthStateListener(listener)
awaitClose {
auth.removeAuthStateListener(listener)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = auth.currentUser == null
)
You are declaring the initial value of your Flow like this:
initialValue = auth.currentUser == null
So before the async callback completes, your isUserSignedOut
will already be true
. As a result, the AuthScreen
will be shown immediately.
You will have to introduce a third state:
While the App performs the asynchronous request, initially set the state to "loading" and display a loading Composable then.
You can create an enumeration to represent the different states:
enum class UIState{
LOADING, LOGGEDIN, NOTLOGGEDIN
}
Then declare your Flow like this:
override fun getAuthState(viewModelScope: CoroutineScope) = callbackFlow {
val listener = FirebaseAuth.AuthStateListener { auth ->
if (auth.currentUser == null) {
trySend(UIState.NOTLOGGEDIN)
} else {
trySend(UIState.LOGGEDIN)
}
}
auth.addAuthStateListener(listener)
awaitClose {
auth.removeAuthStateListener(listener)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = UIState.LOADING
)
And in your Composable:
var uiState by viewModel.getAuthState().collectAsState()
if (uiState.equals(UIState.LOADING) {
// show loading Composable
// (or do nothing if you set the loading Composable as startDestination)
} else if (uiState.equals(UIState.NOTLOGGEDIN) {
// navigate to AuthScreen Composable
} else {
// navigate to MainScreen Composable
}
If you want the logic happen at the very start of the App, you can also set the loading Composable as startDestination
of the NavHost and then navigate when the state becomes LOGGEDIN
or NOTLOGGEDIN
.
You can also write above logic as a when
clause:
when (uiState) {
UIState.LOADING -> {/** show loading Composable **/}
UIState.NOTLOGGEDIN -> {/** navigate to AuthScreen Composable **/}
UIState.LOGGEDIN -> { /** navigate to MainScreen Composable **/ }
}