Antimonit
Antimonit

Reputation: 3264

Android Compose setupWithNavController

I am looking for a Compose variant of setupWithNavController(Toolbar, NavController) to automatically update the up button in an AppBar whenever the Navigation destination changes. So far I haven't found anything useful.

Is it considered a bad design in Compose? Or is there some simple way how to achieve the same thing that I am not seeing?

Upvotes: 7

Views: 2571

Answers (2)

tronman
tronman

Reputation: 10125

You can show the Up button like @Antimonit shows, or this simplified version that does not use the custom previousBackStackEntryAsState() function:

@Composable
fun NavigationIcon(navController: NavController) {
   val backStackEntry by navController.currentBackStackEntryAsState()
   val x = backStackEntry?.destination  // Triggers recomposition

   if (navController.previousBackStackEntry != null) {
      IconButton(onClick = {
         navController.navigateUp()
      }) {
         Icon(Icons.Default.ArrowBack, contentDescription = "Up")
      }
   }
}

Upvotes: 0

Antimonit
Antimonit

Reputation: 3264

I've hacked up a solution, but I am not satisfied.


androidx.navigation:navigation-compose:1.0.0-alpha08 provides an extension function to observe the current back stack entry.

@Composable
fun NavController.currentBackStackEntryAsState(): State<NavBackStackEntry?>

I've created a similar extension to observe the previous back stack entry

/**
 * Gets the previous navigation back stack entry as a [MutableState]. When the given navController
 * changes the back stack due to a [NavController.navigate] or [NavController.popBackStack] this
 * will trigger a recompose and return the second top entry on the back stack.
 *
 * @return a mutable state of the previous back stack entry
 */
@Composable
fun NavController.previousBackStackEntryAsState(): State<NavBackStackEntry?> {
    val previousNavBackStackEntry = remember { mutableStateOf(previousBackStackEntry) }
    // setup the onDestinationChangedListener responsible for detecting when the
    // previous back stack entry changes
    DisposableEffect(this) {
        val callback = NavController.OnDestinationChangedListener { controller, _, _ ->
            previousNavBackStackEntry.value = controller.previousBackStackEntry
        }
        addOnDestinationChangedListener(callback)
        // remove the navController on dispose (i.e. when the composable is destroyed)
        onDispose {
            removeOnDestinationChangedListener(callback)
        }
    }
    return previousNavBackStackEntry
}

and a composable for the back button

@Composable
fun NavigationIcon(navController: NavController): @Composable (() -> Unit)? {
    val previousBackStackEntry: NavBackStackEntry? by navController.previousBackStackEntryAsState()
    return previousBackStackEntry?.let {
        {
            IconButton(onClick = {
                navController.popBackStack()
            }) {
                Icon(Icons.Default.ArrowBack, contentDescription = "Up button")
            }
        }
    }
}

I had to return a @Composable (() -> Unit)? (instead of no return value common to composable functions) because the TopAppBar uses the nullability to check whether to offset the title by 16dp (without icon) or by 72dp (with icon).

Finally, the content looks something like this

@Composable
fun MainContent() {
    val navController: NavHostController = rememberNavController()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Weather") },
                navigationIcon = NavigationIcon(navController)
            )
        },
    ) {
        NavHost(
            navController,
            startDestination = "list"
        ) {
            composable("list") {
                ...
            }
            composable("detail") {
                ...
            }
        }
    }
}

List: list

Detail: detail

It might be cleaner to create a custom NavigationTopAppBar composable and hoist the NavController out of NavigationIcon but the idea stays the same. I didn't bother tinkering further.


I've also attempted to automatically update the title according to the current NavGraph destination. Unfortunately, there is not a reliable way to set a label to destinations without extracting quite a big chunk of internal implementation out of androidx.navigation:navigation-compose library.

Upvotes: 6

Related Questions