raamcosta / compose-destinations

Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate.
https://composedestinations.rafaelcosta.xyz
Apache License 2.0
3.23k stars 134 forks source link

DestinationsNavHost 'start' parameter ruins Multiple Back Stack #667

Closed Velord closed 4 months ago

Velord commented 4 months ago

https://composedestinations.rafaelcosta.xyz/common-use-cases/bottom-bar-navigation

navController.navigateTo API does not have navigateTo function.

My click on tab is:

  if (navController.currentDestination?.route == navigator.getStartRoute(item)) return
    if (isSelected) {
        // When we click again on a bottom bar item and it was already selected
        // we want to pop the back stack until the initial destination of this bottom bar item
        navController.popBackStack(navigator.getStartRoute(item), false)
        return
    }

    navController.navigate(navigator.getDirection(item).route) {
        // Pop up to the root of the graph to
        // avoid building up a large stack of destinations
        // on the back stack as users select items
//        popUpTo(navigator.getGraph().route) {
//            saveState = true
//        }
        // Avoid multiple copies of the same destination when reselecting the same item
        launchSingleTop = true
        // Restore state when reselecting a previously selected item
        restoreState = true
    }
 }

     getDirection(route: BottomNavigationDestination): Direction = when(route) {
        BottomNavigationDestination.Camera -> CameraRecordingNavGraph
        BottomNavigationDestination.Demo -> DemoDestinationDestination
        BottomNavigationDestination.Settings -> BottomNavigationSettingsDestinationDestination
    }

When line navController.navigate(navigator.getDirection(item).route) ends with 'route' App builds but multiple back stack does not work. Everytime start route will be applied when user clicks on tab.

Let's change line to navController.navigate(navigator.getDirection(item)) 'route' is omitted. App builds. When user clicks on tab error is present:

kotlinx.serialization.SerializationException: Serializer for class 'CameraRecordingNavGraph' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

or

kotlinx.serialization.SerializationException: Serializer for class 'BottomNavigationSettingsDestinationDestination' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

Start tab is DemoDestinationDestination

raamcosta commented 4 months ago

Hi @Velord ! 👋

navigateTo was removed a while ago, it seems like this specific page was not updated. Thank you for reporting, I will update it right away!

There is also a big ⚠️ sign on readme about a change in official library that broke usages of NavController passing in Compose Destinations Direction class. You need to either do navController.navigate(MyDestination(myArgs).route) or a better alternative is to do what's written here and get a hold of a DestinationsNavigator and use that instead of the NavController directly.

As far as the multiple back stack not working, it is not controlled by Compose Destinations. I believe it is most likely that you're missing something to make that work. You can check specifically the sample project of this Github repository. We use multiple back stacks there. Or check this article:

https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134

Velord commented 4 months ago
  1. I can't build your project.
  2. Related article is so simple. I am using nested graphs.

After digging your code in 'sample' line by line I have come to my setup was wrong in a few moments. But I am still convinced that is something wrong in the library.

I made a few changes

private fun onTabClick(
    isSelected: Boolean,
    item: BottomNavigationDestination,
    destinationNavigator: DestinationsNavigator,  **// That line was changed.**
    navigator: BottomNavigator,
    onClick: (BottomNavigationDestination) -> Unit
) {
    if (isSelected) {
        // When we click again on a bottom bar item and it was already selected
        // we want to pop the back stack until the initial destination of this bottom bar item
        destinationNavigator.popBackStack(navigator.getStartRoute(item), false)
        return
    }

    val direction = navigator.getDirection(item)
    destinationNavigator.navigate(direction) {
        // Pop up to the root of the graph to
        // avoid building up a large stack of destinations
        // on the back stack as users select items
        popUpTo(navigator.getSupremeRoute()) {  **// That line was changed.**
            saveState = true
        }
        // Avoid multiple copies of the same destination when reselecting the same item
        launchSingleTop = true
        // Restore state when reselecting a previously selected item
        restoreState = true
    }
    onClick(item)
}

class SupremeNavigator(
    private val navController: NavHostController
) : BottomNavigator{

    override fun getDirection(route: BottomNavigationDestination): Direction = when(route) {
        BottomNavigationDestination.Camera -> CameraRecordingNavGraph
        BottomNavigationDestination.Demo -> DemoDestinationDestination
        BottomNavigationDestination.Settings -> BottomNavigationSettingsDestinationDestination
    }

    override fun getGraph(): NavHostGraphSpec = BottomNavigationNavGraph

    @Composable
    override fun CreateDestinationsNavHostForBottom(
        navController: NavHostController,
        modifier: Modifier,
        start: BottomNavigationDestination
    ) {
        DestinationsNavHost(
            navGraph = BottomNavigationNavGraph,
            modifier = modifier,
            //start = getDirection(start), **// Uncomment it for breaking multiple backstack**
            navController = navController,
            dependenciesContainerBuilder = {
                dependency(SupremeNavigator(navController = navController))
            }
        )
    }

    override fun getStartRoute(route: BottomNavigationDestination): TypedDestinationSpec<*> = when(route) {
        BottomNavigationDestination.Camera -> CameraRecordingNavGraph.startDestination
        BottomNavigationDestination.Demo -> DemoDestinationDestination
        BottomNavigationDestination.Settings -> BottomNavigationSettingsDestinationDestination
    }

    override fun getSupremeRoute(): Direction = NavGraphs.bottomNavigationGraph **// Added that**
}

So when I uncomment start = getDirection(start) multiple back stack does not work. Without that line all works as I expect.

What do you think it may be ?

raamcosta commented 4 months ago

use this snippet to see how the back stack looks like with every action you take. Hope that helps!

sorry I don’t have time to check your solution, and I’m positive this is not caused by this library. Please do let me know once you make it work 🙂

@SuppressLint("RestrictedApi")
@Composable
fun LogBackStack(navController: NavController) {
    LaunchedEffect(navController) {
        navController.currentBackStack.collect {
            it.print()
        }
    }
}

fun Collection<NavBackStackEntry>.print(prefix: String = "stack") {
    val stack = toMutableList()
        .map {
            val route = it.route()
            val args = runCatching { route.argsFrom(it) }.getOrNull()?.takeIf { it != Unit }?.let { "(args={$it})" } ?: ""
            "$route$args"
        }
        .toTypedArray().contentToString()
    println("$prefix = $stack")
}
raamcosta commented 4 months ago

Our library is very shallow wrapper around official library. It generates code to have type safe routes, but the APIs you’re calling are the official ones, even if indirectly.

Compose Destinations doesn’t do any additional work besides just calling official libraries and passing the route of whatever the type safe object you’re using.

We have plenty of people using multiple back stacks. The linked article even if simple, has the same principle you have to follow, nothing really changes if you’re using graphs instead of normal destinations.

Where you call the LogBackStack function is also important. Make sure the LaunchedEffect inside is only triggered once.

raamcosta commented 4 months ago

Really wish I had time to check your implementation, I’m sure we’d be able to get to the bottom of this. Maybe try hitting Kotlin slack, maybe try to replace Compose Destination specific APIs just so more people can follow your implementation. Someone will be able to help you!

good luck 🤞

Velord commented 3 months ago

I fixed log spamming issue. It is not related to this issue.

https://github.com/Velord/ComposeScreenExample I have added step by step Vanilla Compose navigation library. And it works as expected with the same navigation patterns.

Project uses navigation libs:

This line ruins multiple back stack for sure. //start = getDirection(start), // Fixme: this is not working