androidkotlinandroid-jetpack-composeandroid-jetpackandroid-jetpack-navigation

How to not display a destination in jetpack navigation?


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
)

Solution

  • 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 **/ }
    }