Jetpack Compose: Bottom bar navigation not responding after deep-linking

Viewed 1010

I have setup a bottom bar in my new Jetpack Compose app with 2 destinations. I have tried to follow the samples from Google.

So for example it looks something like this:

@Composable
fun MyBottomBar(navController: NavHostController) {
    val items = listOf(
        BottomNavigationScreen.ScreenA,
        BottomNavigationScreen.ScreenB
    )
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination

    BottomNavigation {
        items.forEach { screen ->
            BottomNavigationItem(
                onClick = {
                    navController.navigate(screen.route) {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }

                        launchSingleTop = true
                        restoreState = true

                    }
                },
                selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
                icon = { Icon(imageVector = screen.icon, contentDescription = null) },
                label = { Text(stringResource(screen.label)) }
            )
        }
    }
}

This all works fine and I'm able to navigate between the two destinations. However, I also have a deep-link to ScreenB. Once this has been invoked, pressing the ScreenA button seems to do nothing (If I add logging I can see that currentDestination is being repeatedly set to ScreenB) but pressing back returns to the startDestination of ScreenA.

My workaround at the moment is to remove the restoreState = true line from the sample code.

My suspicion is that something about the deep-link is being persisted and although it tries to go to ScreenA the navigation component says that it's got a deep-link pointing to ScreenB so it just goes there. I've tried resetting the activity intent so that it has no flags and no data in the intent, I've even tried changing the intent action type but all to no avail.

I am using Compose 1.0.0-rc02 and Compose Navigation 2.4.0-alpha04.

Am I doing something wrong or is this a bug?

2 Answers

I know you got this code from the official documentation, but I don't think it works well for bottom navigation. It keeps the elements in the navigation stack, so pressing the back button from ScreenB will take you back to ScreenA, which does not seem to me to be the right behavior in this case.

That's why it's better to remove all elements from the stack, so that only one of the tabs is always left. And with saveState you won't lose state anyway. This can be done as follows:

fun NavHostController.navigateBottomNavigationScreen(screen: BottomNavigationScreen) = navigate(screen.route) {
    val navigationRoutes = BottomNavigationScreen.values()
        .map(BottomNavigationScreen::route)
    val firstBottomBarDestination = backQueue
        .firstOrNull { navigationRoutes.contains(it.destination.route) }
        ?.destination
    if (firstBottomBarDestination != null) {
        popUpTo(firstBottomBarDestination.id) {
            inclusive = true
            saveState = true
        }
    }
    launchSingleTop = true
    restoreState = true
}

And use it like this:

BottomNavigationItem(
    onClick = {
        navController.navigateBottomNavigationScreen(screen)
    },
    selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
    icon = { Icon(imageVector = screen.icon, contentDescription = null) },
    label = { Text(screen.label) }
)

For the same reasons, I wouldn't use deep link navigation in this case. Instead, you process them manually. You can use a view model to not re-process deep link if you leave bottom navigation view and come back:

class DeepLinkProcessingViewModel : ViewModel() {
    private var deepLinkProcessed = false

    fun processDeepLinkIfAvailable(context: Context): String? {
        if (!deepLinkProcessed) {
            val activity = context.findActivity()
            val intentData = activity?.intent?.data?.toString()

            deepLinkProcessed = true
            return intentData
        }
        return null
    }
}

And using this view model you can calculate start destination like this:

val context = LocalContext.current
val deepLinkProcessingViewModel = viewModel<DeepLinkProcessingViewModel>()
val startDestination = rememberSaveable(context) {
    val deepLink = deepLinkProcessingViewModel.processDeepLinkIfAvailable(context)
    if (deepLink == "example://playground") {
        // deep link handled
        BottomNavigationScreen.ScreenB.route
    } else {
        // default start destination
        BottomNavigationScreen.ScreenA.route
    }
}
NavHost(navController = navController, startDestination = startDestination) {
    ...
}

Or, if you have several navigation elements in front of the bottom navigation and you don't want to lose them with a deep link, you can do it as follows:

val navController = rememberNavController()
val context = LocalContext.current
val deepLinkProcessingViewModel = viewModel<DeepLinkProcessingViewModel>()
LaunchedEffect(Unit) {
    val deepLink = deepLinkProcessingViewModel.processDeepLinkIfAvailable(context) ?: return@LaunchedEffect
    if (deepLink == "example://playground") {
        navController.navigateBottomNavigationScreen(BottomNavigationScreen.ScreenB)
    }
}
NavHost(
    navController = navController,
    startDestination = BottomNavigationScreen.ScreenA.route
) {

findActivity:

fun Context.findActivity(): Activity? = when (this) {
    is Activity -> this
    is ContextWrapper -> baseContext.findActivity()
    else -> null
}

Looks like it's finally fixed in the 2.4.0-beta02 release; so it was a bug after all.

I was able to add the saveState and restoreState commands back into my BottomBar (as per the documentation) and following a deep-link I was now still able to click the initial destination.

Related