I sent this in a PM to Ian Lake directly, but send...
# compose-android
b
I sent this in a PM to Ian Lake directly, but sending here to see if anyone else has any insight? Hey Ian, do you have any update on Navigating Navigation, but with Jetpack Compose? I'm trying to implement a setup where I've got my startDestination as an authenticated route but that route is wrapped with a Composable that checks the stateHolder variable of isLoggedIn and displays content, otherwise it navigates to the unauthenticatedGraphRoute. One issue I'm running into, is that anytime I navigate, even to a Composable that is not route protected, I end up in this composable and it creates another unauthenticatedGraph, so I was basically ending up in an infinite loop where the UI for the login page would spazz out. I fixed that by doing a launchedEffect, but even though I was navigating to authenticatedGraph and popping everything up to that graph, I was still ending up with an extra authenticatedGraph start destination because it would hit the else block and add another destination, this broke my login logic which was to basically take the previousBackStackEntrys route and navigate to it. In the situation of logging out I was hoping there would be no prevoiusBackStackEntry so that it would do the default fallback and just navigate to the authenticatedGraphRoute. this is the current state of my RequiredLoggedIn composable which seems to be working-ish but is hacky.
Copy code
@Composable
fun RequireLoggedIn(
    navController: NavController,
    appState: FishbowlAppState,
    content: @Composable () -> Unit
) {
    if (appState.isLoggedIn) {
        content()
    } else {
        if (navController.currentBackStackEntry?.destination?.parent?.route != unauthenticatedRoutePattern) {
            navController.navigate(unauthenticatedRoutePattern)
        }
    }
}
that extra if block inside is basically preventing the navigation loop from happening if the backStackEntry destination parent is already on the unauthenticatedRoutePattern, but it's unfortunate that this is even getting triggered at all, since it's a Composable that's wrapped around a authenticatedRoute screen... Another thing I had to do to prevent an infinite loop when pressing the back button from the login screen (with the restricted screen on the backstack still) was to override the backHandler in the login screen like this
Copy code
val onBackPressedCallback = remember {
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                // Close the app
                (context as? Activity)?.finishAffinity()
            }
        }
    }

    // Register the onBackPressedCallback
    BackHandler {
        onBackPressedCallback.handleOnBackPressed()
    }
even in the Navigating Navigation video where you say to simply popBackStack in order to get to the startDestination once authenticated, you don't really cover preventing this issue, is the backHandler in the loginScreen the right approach? lastly here's my logic for onLoginSuccess, which I thought was pretty robust until I was getting the issue where RequireLoggedIn was getting triggered and putting another authenticatedGraph on the backStack (even though I was launching single top) and with a launchedEffect.
Copy code
onLoginSuccess = {
//                     Extra protection in case the login is performed twice somehow.
//
//                     A good sign that this method is working is that we should be able
//                     to have our start destination as the authenticatedGraph or the unauthenticatedGraph,
//                     and the app behavior is exactly the same.
//
//                     This statement is made without taking into consideration future features such
//                     as deep linking or remembering the last screen the user was on before logging out.
                    if (!appState.isLoggedIn) {
                        appState.isLoggedIn = true

                        val previousDestination = navController.previousBackStackEntry?.destination
                        previousDestination?.route?.let { route ->
                            // Navigate to the previous destination
                            // This is safer compared to popBackStack() since it will not
                            // throw an exception if the back stack is empty
                            // or allow the user to double tap the login button and have it pop
                            // the back stack twice
                            navController.navigateToRouteAndPopUpToGraph(route)
                        } ?: navController.navigateToAuthenticatedGraph()
                        // ^ Navigate to the authenticatedGraph without a specific route
                        // This would be called if login was to happen and there was no previous route
                        // to navigate back to, i.e. if the backStack was cleared when logging out
                        // or if the startDestination was set to the unauthenticatedGraph
                    }
                })
This whole process of converting over from having my start destination being the unauthenticatedGraph to the authenticatedGraph but with a wrapper that redirects to tha unauthenticatedGraph has been pretty frustrating. Nothing in my app besides the unauthenticatedGraph, which contains the loginScreen and the account creation screen should be accessible without being logged in. and I wanted to update my implementation to what you suggested in Navigating Navigation in order to better support deep linking in the future etc.. Also, now that I've been pondering on this for a few days, I'm wondering if it would just be better to have two separate navHosts and to catch this at the main App composable level or something.
đŸ§” 7
t
b
@Tim Malseed looking forward to checking that out when I get a minute, thank you.
s
That’s an interesting article Tim. One thing this solution doesn’t solve is that then if you go to “login” screen, pressing back breaks the predictive back gesture, so the user won’t be able to know where they will end up in after they finish pressing back.
Another thing I see in your suggestion Tim, is that you move the navigation logic to
HomeScreen
. But now if we’ve deep linked into some other screen, you need to again check for login status on that screen too. And this blows up on checking on every single place in the app. What are you doing instead of that? Could this check not be moved top level, outside of any destination? Probably where the NavHostController is first constructed?
t
Good observations/questions @Stylianos Gakis. I don’t have an answer off the top of my head.. it’s been a while since I wrote this one. Definitely worth hoisting the navigation logic out of the HomeScreen if you intend to reuse it. Another downside of placing this logic in the HomeScreen, is if you navigate back to the home screen after conditionally navigating away from it, and that condition hasn’t updated by the time you come back, you might get navigated forward again - resulting in a navigation loop
I’m not sure about the predictive back gesture, I haven’t given it any thought and I’m not even sure it was a thing when I was writing this
Definitely time for a follow up article, I feel like I’ve learned a lot since writing this
One important lesson I learned is that the goal shouldn’t be ‘don’t have multiple nav hosts’ Multiple nav hosts are bad/confusing if they are nested (i.e. one can live inside of another). Because it becomes hard to know which nav host should be responsible for a certain piece of navigation. But having neighbouring navhosts is OK IMO. I think it’s OK to do:
Copy code
@Composable
fun RootComposable {
    if (isLoggedIn) {
        MainNavHost()
    } else {
        AuthNavHost()
    }
}
Since only one nav host will ever be rendered at any one time
Perhaps this approach doesn’t lend itself to deeplinking? In my case, we don’t have any deep links anyway!
s
Hmm, I really think this isn’t the lesson to learn here. I’m fairly certain that the suggestion is to not use multiple NavHosts no matter what. In this situation, what happens if you deep link to a detail screen which is supposed to be in a logged in scenario? Do you just drop the deep link completely and let the user login?
Beat me to it, but yeah multiple NavHosts aren’t the solution there.
t
Well, they can be.
Trying too hard to follow Google’s advice resulted in a lot of pain for me. They don’t have the full context of your real-world scenarios, and apps like NIA don’t have to deal with complex conditional navigation, so it’s hard to know how well-tested this advice is. Mixing the imperative navigation style of navigation-compose with the reactive nature of Compose itself is really difficult. There are lots of little gotchas. If you were to ‘hoist’ the conditional navigation logic away from one of your destination screens, like you suggested before, you’re also going to run into issues with deep linking.
So honestly the lesson I learned in this journey is don’t try too hard to follow Google’s advice. It’s not always given with the same context, it can change over time, and it occasionally contradicts itself!
I still try to avoid multiple nav hosts myself, but not at all costs
s
If you were to ‘hoist’ the conditional navigation logic away from one of your destination screens, like you suggested before, you’re also going to run into issues with deep linking.
How so? What scenario are you thinking about here? Trying to understand better myself too tbh, since I am sure I haven’t thought of everything obviously.
t
Well, instead of handling the conditional navigation at the destination - arriving at the destination, navigating to the conditional screen, and then coming back - You’d be intercepting this at some higher-level, before arriving at the destination, and taking the user elsewhere, then you’ve lost track of where they intended to go.
I’m sure there are other clever ways to handle this, like some sort of reusable composable function that you drop into each destination that requires the conditional check
I’m mostly just talking off the top of my head, and like I said, not currently dealing with deeplinks, so forgive me if this doesn’t make any sense 😓
s
You’d be intercepting this at some higher-level, before arriving at the destination, and taking the user elsewhere, then you’ve lost track of where they intended to go.
Not necessarily. NavHost handles the deep link immediately as it comes from the activity’s intent. So this will happen asap. In the meantime, you’re probably launching a coroutine on the Activity lifecycle scope, and you do a repeatOnLifecycle(Started) in which you are doing this check for the Auth status. So, so far we’ve navigated to the deep link successfully, but now are about to receive the fact that we are logged out. We get that status that we’re logged out in this async manner, and we can then pop the entire backstack, saving the state as we do so, and navigate to the login screen. In there, if you press back, you can exit the app normally, since you’re at the top of the backstack. As you finish logging in, you can now navigate back to the start destination, while popping the login screen from the backstack, and while also
restoreState = true
. This should bring you back to where you were before, and now you are also logged in. Now that async check if you’re logged in just returns true and you don’t do anything with it. For that brief moment where you’ve deep linked somewhere, but you’ve not yet checked the auth status, you can do something like use the splash screen APIs with
setKeepOnScreenCondition
to not hide it until you’ve gotten some response from it, be it logged in, or logged out. Wouldn’t something like this fix more or less your concerns about losing your deeplinked destination? And no need to do a reusable thing for all app screens either. I haven’t tested this entire setup myself either, since unfortunately I’m still fighting to remove some legacy Activities which handle login now, but this is toward where I’d try to steer my implementation. We’ve been using this state restoration for bottom navigation items and it seems to work very well, so I don’t see why it wouldn’t work for this scenario too.
t
Yeah, I’m not sure - this really deserves some more thought, a demo project and maybe a follow up post. The idea of using the state restoration APIs to solve this problem is also a relatively new one, I remember it being mentioned in a recent video. I haven’t given it much thought tbh
b
@Tim Malseed @Stylianos Gakis Do either one of you have a link to the article or video that mentions that we shouldn't use multiple navHosts? It's pretty tempting to consider it after reading through this and it seeming like there's not a clear (to me) way to handle this at the moment.
@Tim Malseed Agreed on the demo project, I feel like auth is always left out of demo projects probably for complexity and then also there's different setups to accommodate like having the app being mostly accessible without auth (Twitter) and other apps that you can't do much without authing (my company's app for example). But it'd be super awesome if they just had a simple server you had to setup locally (or use like an API key or something) to use alongside a demo project. Or not even use an API, just mimic an auth flow.