Home > Mobile >  Jetpack Compose Navigation - Bottom Nav Multiple Back Stack - View Model Scoping Issue
Jetpack Compose Navigation - Bottom Nav Multiple Back Stack - View Model Scoping Issue

Time:10-04

So I have two tabs, Tab A and Tab B. Each tab has its own back stack. I implemented the multiple back stack navigation using code in this google docs

    val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.route) {
              // Pop up to the start destination of the graph to
              // avoid building up a large stack of destinations
              // on the back stack as users select items
              popUpTo(navController.graph.findStartDestination().id) {
                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
            }
          }
        )
      }
    }
  }
) { 
  NavHost(navController, startDestination = A1.route) {
    composable(A1.route) { 
       val viewModelA1 = hiltViewModel() 
       A1(viewModelA1) 
    }
    composable(A2.route) { 
       val viewModelA2 = hiltViewModel() 
       A2(viewModelA2) 
    }
    composable(A3.route) { 
       val viewModelA3 = hiltViewModel() 
       A3(viewModelA3) 
    }
  }
}

Tab A has 3 screens (Screen A1 -> Screen A2 -> Screen A3). I use the hiltViewModel() function to instantiate the view model and I invoked it inside the composable() block for each screen

The issue is when I'm navigating from A1 to A2 to A3 and then when I change tab to Tab B, the view model for Screen A2 seems like it's being disposed (onCleared is called). So when I go back to Tab A displaying Screen A3 then hit back to Screen A2, the view model for A2 is instantiated again (init block is called again). What I wanted to achieve is to retain the view model for A2 for this flow until I back out of A2.

Is this even possible?

CodePudding user response:

This seems like a bug when you click on the next navigation item too fast, while the current view appear transition is not yet finished. I've reported it, please star it to bring more attention.

Meanwhile you can wait current screen transition to finish before navigating to the next one. To do so you can check visibleEntries variable and navigate only after it contains only single item.

Also I think that current documentation provide not the best example of bottom navigation, because if you're not on a start destination screen, pressing back button will bring you back to the start destination, when I expect the view to be dismissed. So I've changed how you navigate too, if you're fine with the documentation behaviour, you can replace content of fun navigate() with your own.

val navController = rememberNavController()
var waitEndAnimationJob by remember { mutableStateOf<Job?>(null)}
Scaffold(
    bottomBar = {
        BottomNavigation {
            val navBackStackEntry by navController.currentBackStackEntryAsState()
            val currentDestination = navBackStackEntry?.destination
            val scope = rememberCoroutineScope()
            items.forEach { screen ->
                fun navigate() {
                    navController.navigate(screen.route) {
                        val navigationRoutes = items
                            .map(Screen::route)
                        val firstBottomBarDestination = navController.backQueue
                            .firstOrNull { navigationRoutes.contains(it.destination.route) }
                            ?.destination
                        // remove all navigation items from the stack
                        // so only the currently selected screen remains in the stack
                        if (firstBottomBarDestination != null) {
                            popUpTo(firstBottomBarDestination.id) {
                                inclusive = true
                                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
                    }
                }
                BottomNavigationItem(
                    icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
                    label = { Text(stringResource(screen.resourceId)) },
                    selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
                    onClick = {
                        // if we're already waiting for an other screen to start appearing
                        // we need to cancel that job   
                        waitEndAnimationJob?.cancel()
                        if (navController.visibleEntries.value.count() > 1) {
                            // if navController.visibleEntries has more than one item
                            // we need to wait animation to finish before starting next navigation
                            waitEndAnimationJob = scope.launch {
                                navController.visibleEntries
                                    .collect { visibleEntries ->
                                        if (visibleEntries.count() == 1) {
                                            navigate()
                                            waitEndAnimationJob = null
                                            cancel()
                                        }
                                    }
                            }
                        } else {
                            // otherwise we can navigate instantly
                            navigate()
                        }
                    }
                )
            }
        }
    }
) { innerPadding ->
    // ...
}

CodePudding user response:

Found the root cause of this. I was using these dependencies and they don't seem to go together.

  • androidx.hilt:hilt-navigation-compose:1.0.0-alpha03

  • androidx.navigation:navigation-compose:2.4.0-alpha10"

I removed the navigation:navigation-compose dependency and it seemed to work fine now.

  • Related