Original address: medium.com/google-deve…

Jossiwolf.medium.com/

Published: June 7, 2021-14 minutes to read

If you’re developing a mobile application, chances are you’ll need some form of navigation. Navigating well is not easy because there are many challenges: back-stack handling, life cycles, state saving and recovery, and deep linking are just a few of them. In this article, we’ll explore the navigation component’s support for Jetpack Compose and take a look at its internal structure.

Could I have chosen a more cliched picture? Probably not. Photo by Mick Haupt on Unsplash. Thank you, Mick!

Let’s go!

Before we begin, we’ll add a dependency on navigation-compose, which is an artifact of the navigation component that supports compose.

implementation "Androidx. Navigation: navigation - compose: 2.4.0 - alpha02"
Copy the code

Let’s jump into the code

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen()
		}
	}
}

@Composable
fun FeedScreen(...).{... }Copy the code

First, we create and remember a NavController using the rememberNavController method. RememberNavController returns a NavHostController, which is a subclass of NavController and provides some additional apis that NavHosts can use. We’ll use it as a NavController in the future when we talk about NavController and using it, because we don’t need to know about these extra apis ourselves, it’s just important to NavHost. We pass this to our NavHost composable. The NavHost composable is responsible for hosting the content of NavBackStackEntry related NavDestination (we’ll look at the details later!). .

The lambda we pass to NavHost is the builder of our navigation diagram. Here we have access to NavGraphBuilder and can build and declare our navigation diagrams. This is where we declare our destination and nested diagrams. If you’re from an “old” navigation library, it might feel a little strange at first! There is no XML anymore, not even one. There was no XML, not even a navigation chart. Although the Kotlin DSL has been around for a long time, it has been eclipsed by XML so far.

It also means that we won’t have graphic visual representation for the foreseeable future. Renderings of XML navigation diagrams are very useful, so let’s hope we get something like this for Compose at some point!” .

Declaring a composable destination is easy: we use the composable method provided by navigation-compose.

// From https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-compose/src/main/java/ androidx/navigation/compose/NavGraphBuilder.kt
public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    addDestination(
        ComposeNavigator.Destination(provider[ComposeNavigator::class].content).apply {
            this.route = route
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}
Copy the code

It is an extension function of NavGraphBuilder and is essentially a convenient wrapper around the addDestination method of NavGraphBuilder. Here, a ComposeNavigator specific NavDestination is created. Composenavigators are navigators that handle post-stack and composable navigation.

The NavGraphBuilder is part of the generic (non-specific) navigation API and basically provides an addDestination method to add a destination to a navigation graph.

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen()
		}
	}
}

@Composable
fun FeedScreen(...).{... }Copy the code

Looking back at our code, we passed the composable function a route that told the NavGraphBuilder what route we wanted to use to navigate to the destination. If you are coming from an “old” navigation library, the route is roughly equivalent to defining an ID for a destination. As of version 2.4.0 of the navigation library, the ID of the NavDestination is automatically set based on its route (and updated every time the route is updated), so there is no need to define an ID. The last argument to the Composable function is a @composable lambda, which will be set to the content of the destination. When we navigate to the destination, NavHost will host the composable.

In our @composable lambda, we just host the Composable of the FeedScreen. While you could theoretically declare all of your content directly in a navigation diagram, don’t do that. Things can get messy quickly, and cleaning up can be tedious!

That’s great! We’ve created our navigation controller. We have created our NavController, using the composable NavHost and composable NavGraphBuilder functions to create a composable destination and add it to the navigation diagram. Let’s add a second destination and navigate!

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen(navController)
		}
		composable(route = "adopt") {
			AdoptionScreen()
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) {
	Button(onClick = { navController.navigate("adopt") }) {
		Text("Click me to adopt!")}}@Composable
fun AdoptionScreen(...).{... }Copy the code

Let’s see what’s changed.

First, we add a new destination to the diagram and a composable AdoptionScreen. To navigate when the FeedScreen button is clicked, we added NavController to the parameters of the FeedScreen. Finally, when the button is clicked, we call NavController#navigate to enter the path of the destination we want to navigate.

The episode. Under the hood

If you don’t like the nuts and bolts of working under the hood, feel free to skip to the next section. This episode provides a high-level overview that is useful for your understanding of navigation-composition.

When we call navigate, the NavController calculates what it needs to do to get us to our destination. First, it checks if there is a destination associated with the request route and if it is in the navigation diagram. We don’t want to navigate to the depths of interstellar space

Note: When using route navigation, this is internally treated as a deep link. This also means that if you use Navigation Kotlin DSL and register destinations with routes instead of IDS, you can get deep links for free.

After the NavController determines that the destination we want to navigate exists, the NavController looks at all the navigation options (should the back end stack pop? Should destinations be launched on a single roof?) , creates a NavBackStackEntry if the destination is not already on the back stack (that is, when we haven’t navigated to it or popped it up before), and retrieves the back stack entry if the destination is on the back stack. This may be the case when we have the following flow.

Navigation flowchart with “Feed “start destination and “Adoption” destination. First, we navigate from the “Feed” to the “Adoption “destination, and then back to the “Feed”.

A back stack entry is created and added to the back stack for our “Feed “start destination. When we navigate to the “Adopt” screen, a post-stack entry is created for the adopt screen. When we navigate to “Feed “, we already have the “Feed “back stack entry in the back stack. Just like Fragments, Activities, or other components we know of in Android development, NavBackStackEntry has a life cycle so that a back Stack entry can stay on the back stack without being active, That is, because it’s not on top of the back stack.

For navigation, the NavController looks at the Navigator for the requested NavDestination. In our case, that is the ComposeNavigator because we are navigating to a composable destination. The NavController calls the navigate function of the navigator with the requested destination and navigation options to execute the navigator’s navigation logic and add the back stack entry to the back stack if needed. Finally, this entry is added to the back stack of the NavController. A navigator only has the post-stack items it knows how to handle (items created from the navigator’s destination), and the NavController maintains the post-stack of the entire graph. You can think of NavController as the big boss and navigator as the little boss. In addition, the navigator (or, more accurately, the navigator’s NavigatorState) moves the post-stack entry to the RESUMED state because it is now ready to be displayed. NavigatorState also moves the state of the previous entry to the CREATED state, indicating that it is no longer active.

Meanwhile, the composable NavHost looks at the ComposeNavigator rear stack, which now has added or updated NavBackStackentries. It recombines with an updated list of post-stack items and emits the target content of each post-stack item (the @composable lambda we passed in earlier) into the composition. In short, it calls the @composable lambda that we passed to the Composable function in the NavGraphBuilder.

Then, we arrived at our destination! 🎉.

Let’s go back to our code

Well, our little interlude is over, so let’s go back to our code. In our example, we didn’t specify any navigation options while navigating — we just called the navigation with the route.

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen(navController)
		}
		composable(route = "adopt") {
			AdoptionScreen()
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) {
	Button(onClick = { navController.navigate("adopt") }) {
		Text("Click me to adopt!")}}@Composable
fun AdoptionScreen(...).{... }Copy the code

By default, the previous destination (our FeedScreen destination) is kept in the later stack. When we want to go back from the AdoptionScreen, we just pop that destination off the back stack.

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = "feed") {
		composable(route = "feed") {
			FeedScreen(navController)
		}
		composable(route = "adopt") {
			AdoptionScreen(navController)
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) {
	Button(onClick = { navController.navigate("adopt") }) {
		Text("Click me to adopt!")}}@Composable
fun AdoptionScreen(navController: NavController) { 
	Button(onClick = { navController.popBackStack("adopt", inclusive = true) }) {
		Text("Click me to go back!")}}Copy the code

Instead of defining where we want to go, we tell the NavController that the destination on the adopted route is the topmost destination and that we should pop the back stack by setting inclusivity to true. Note that this is the default behavior, popBackStack navController. So we can directly call popBackStack () without any parameters. That way, we can navigate to the adopted route from anywhere and always come back to where we came from. Alternatively, we can use navController.navigateUp() here. It tries to navigate up the navigation hierarchy. In most cases, this means popping the current item from a later stack, but if the app was opened by a deep link, using navigateUp can make sure you get back to where you came from, for example, another app.

And so we have the most basic form of navigation. Let’s clean up the code!

Clean up the

As you can imagine, repeating our route where we want to navigate is not a very scalable approach. We want to be able to reuse these routes. This will prevent us from introducing errors due to typos and help us when we want to change our navigation logic. There are several ways to do this, which is to define all routes as constants in one object (or objects).

object AppDestinations {
	const val Feed = "feed"
	const val Adopt = "adopt"
}
Copy the code

This is the approach we used initially, but I prefer Chris Banes’ implementation using sealed classes. It’s easier to read and generally easier to maintain.

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Adopt: Screen("adopt")}Copy the code

Kotlin 1.4 and 1.5’s relaxed rules for sealed classes allow for a clean separation of these definitions, making sealed classes perfectly appropriate here.

Let’s update our previous code to use it

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Adopt: Screen("adopt")}Copy the code
@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(navController)
		}
		composable(route = Screen.Adopt) {
			AdoptionScreen(navController)
		}
	}
}

@Composable
fun FeedScreen(navController: NavController) {
	Button(onClick = { navController.navigate(Screen.Adopt) }) {
		Text("Click me to adopt!")}}@Composable
fun AdoptionScreen(navController: NavController) { 
	Button(onClick = { navController.popBackStack(Screen.Adopt, inclusive = true) }) {
		Text("Click me to go back!")}}Copy the code

parameter

When navigating to a destination, we often pass an ID or other parameter to load specific data. Let’s say our FeedScreen now has a list of cute puppies for adoption, and we want to display adoption pages for that particular puppy when we click on it.

@Composable
fun FeedScreen(navController: NavController) {
	val viewModel = ...
	val dogs by viewModel.allDogs.collectAsState()
	LazyColumn {
		items(dogs) { dog ->
			DogCard(
				dog, 
				onClick = { navController.navigate(Screen.Adopt) }
			)
		}
	}
}

@Composable
private fun DogCard(dog: Dog, onClick: (dog: Dog) - >Unit){... }Copy the code

Our screen.adopt destination doesn’t know how to handle parameters yet, so let’s jump back to our route and add a parameter first.

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Adopt: Screen("dog/{dogId}/adopt")}Copy the code

Parameters are defined in curly braces, which contain the name of the parameter. We will use this name to retrieve parameters later. The braces register it as a placeholder, which is the expected location of the parameter when the route is created.

Of course, the format of the route is entirely up to you, but it makes sense to follow a RESTful URL design. Think of routes as screen identifiers. It should be unique, clear, and easy to understand.

For required parameters, the parameters are defined as path parameters. For our Adopt route, we always require the presence of dogId, so we define it as a path parameter. If we want to provide optional arguments, we will use the syntax of the query argument: adoption? DogId = {dogId}.

Back in our navigation diagram builder, the @composable lambda of the compose function requires a single argument. NavBackStackEntry for the destination. Among other important information, NavBackStackEntry holds parameters extracted from the route being navigated.

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(navController)
		}
		composable(route = Screen.Adopt) { backStackEntry ->
			valdogId = backStackEntry.arguments? .getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(navController, dogId)
		}
	}
}

@Composable
fun FeedScreen(navController: NavController){... }@Composable
fun AdoptionScreen(navController: NavController, dogId: String){... }Copy the code

From the post-stack entry, we can extract the dogId that we declared as a parameter in the route. Note that the parameter to this entry is empty, but we can be sure that this data is here because it is part of the route we defined. When navigating, the requested route must exactly match the destination’s route or its pattern. Since our dogId parameter is part of the route, we can’t get into the sticky situation of losing it. Still, it’s a good idea to consider handling the situation, rather than sweeping it away with a simple nonempty assertion.

Now that we have our navigation destination Settings, we can add parameters to our navigation calls in the feed! To do this, we must modify our parameters. To do this, we must modify the route we want to navigate. We used screen.adopt as a route, but later updated this route as a template, so we can’t add our parameters here. Instead, we can create a createRoute function with the required parameters, which will establish the route. Thanks to the idea of Chris Banes.

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Adopt: Screen("{dogId}/adopt") {
		fun createRoute(dogId: String) = "$dogId/adopt"}}Copy the code
@Composable
fun FeedScreen(navController: NavController) {
	val viewModel = ...
	val dogs by viewModel.allDogs.collectAsState()
	LazyColumn {
		items(dogs) { dog ->
			DogCard(
				dog, 
				onClick = { selectedDog -> 
					navController.navigate(Screen.Adopt.createRoute(selectedDog.id)
				}
			)
		}
	}
}
Copy the code

You can find more information about navigation with parameters, optional parameters, and parameter types other than strings in the official Android developer documentation.

Put our navigation calls in the same place

Cool, everything looks good and it’s working well, right? HMM… On the first few screens. As our applications become more complex, we want to navigate from multiple places to a destination. We will eventually pass the NavController to at least every composable at the screen level. This creates a dependency between the Composable and NavController, making it more difficult to test and create @previews. The test guide for navigation-composition also illustrates this.

Developer.android.com/jetpack/com…

In addition to testing, this makes it harder to change our navigation logic. If we navigate to our adoption screen from five different places, navController.navigate is called in each place, and if we want to add a destination or otherwise change the navigation logic, such as popping up the back end stack, we are at best annoying and at worst difficult to update the parameters for that destination, And it creates bugs. The magic phrase is “common location of navigation calls “– we want to make sure that all our navigation calls are in one place, not scattered across 30 different combinations.

Update our code. Instead of passing NavController to FeedScreen and AdoptionScreen, we can change them to accept a lambda.

@Composable
fun AdoptionScreen(dogId: String, navigateUp: () -> Unit) {
	Button(onClick = navigateUp) {
		Text("Click me to go back!")}}Copy the code
@Composable
fun FeedScreen(showAdoptionPage: (dogId: String) - >Unit) {
	val viewModel = ...
	val dogs by viewModel.allDogs.collectAsState()
	LazyColumn {
		items(dogs) { dog ->
			DogCard(
				dog, 
				onClick = { dog -> showAdoptionPage(dog.id) }
			)
		}
	}
}
Copy the code

After that, we updated our CuteDogPicturesApp to deliver the lambda instead.

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(
				showAdoptionPage = { dogId ->
					navController.navigate(Screen.Adopt.createRoute(dogId))
				}
			)
		}
		composable(route = Screen.Adopt) { backStackEntry ->
			valdogId = backStackEntry.arguments? .getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(
				dogId, 
				navigateUp = { navController.popBackStack(Screen.Adopt, inclusive = true)})}}}Copy the code

Thus, we have decoupled our composability from the actual navigation dependencies, making it easy to fake this behavior and easily refactor it later. We can even extract the actual navigation call into a local function if we wish. Since different destinations may require different navigation logic (you might want to pop up the back stack when you’re navigating from screen B to C, but not when you’re navigating from A to C), I recommend holding off on this (probably) immature optimization for now. With the common positioning of navigation calls, you already have a good starting point if you want to extract more later.

Nested navigation diagrams

Using nested navigation diagrams, we can group and modularize a set of destinations. If you’ve used nav components before, you probably know the
XML tag, which can be used to declare a nested navigation chart. With the navigation DSL, there is a navigation extension function similar to the composable extension provided by navigation-compose. This navigation extension is provided by generic (not combination-specific) navigation artifacts.

sealed class Screen(val route: String) {
	object Feed: Screen("feed")
	object Dog: Screen("dog/{dogId}")}sealed class DogScreen(val route: String) {
  	object Adopt: DogScreen("dog/{dogId}/adopt") {
		fun createRoute(dogId: String) = "dog/$dogId/adopt"
	}
  	object ContactDetails: DogScreen("dog/{dogId}/contactDetails") {
		fun createRoute(dogId: String) = "dog/$dogId/contactDetails"}}Copy the code
@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(
				showAdoptionPage = { dogId ->
					navController.navigate(DogScreen.Adopt.createRoute(dogId))
				}
			)
		}
		navigation(route = Screen.Adopt, startDestination = DogScreen.Adopt) {
		    composable(route = DogScreen.Adopt) { backStackEntry ->
			valdogId = backStackEntry.arguments? .getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(
				dogId,
				navigateUp = { navController.popBackStack(DogScreen.Adopt, inclusive = true) }
			)
		    }
		    composable(route = DogScreen.ContactDetails) { backStackEntry ->
			valdogId = backStackEntry.arguments? .getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionContactDetailsScreen(dogId)
		    }
		}
		// imagine 30 more of these in here!}}Copy the code

To declare a nested graph, we call the navigation method with the path of the nested graph so that it can be navigated to and the starting destination can be set. We also introduced a DogScreen containment class that represents the route in this diagram. The Screen containment class now represents the top-level destination, and nested destinations are defined in their own containment class. This makes the code easier to read and maintain because more destinations are added. We can also move the DogScreen class into its own file for more separation

Extract navigation chart

As your application grows, so does your navigation chart. Eventually, you’ll need nested navigation and end up with a very long navigation diagram definition that is difficult to read and maintain.

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
		composable(route = Screen.Feed) {
			FeedScreen(
				showAdoptionPage = { dogId ->
					navController.navigate(Screen.Adopt.createRoute(dogId))
				}
			)
		}
		navigation(route = Screen.Dog, startDestination = DogScreen.Adopt) {
		    composable(route = DogScreen.Adopt) { backStackEntry ->
			valdogId = backStackEntry.arguments? .getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(
				dogId,
				navigateUp = { navController.popBackStack(DogScreen.Adopt, inclusive = true) }
			)
		    }
		    composable(route = DogScreen.ContactDetails) { backStackEntry ->
			valdogId = backStackEntry.arguments? .getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionContactDetailsScreen(dogId)
		    }
		}
		// imagine 30 more of these in here!}}Copy the code

Since the composable and navigation functions are just extensions to NavGraphBuilder, we can also use extension functions to decompose our navigation diagrams.

@Composable
fun CuteDogPicturesApp(a) {
	val navController = rememberNavController()
	NavHost(navController, startDestination = Screen.Feed) {
	        addFeedGraph(navController)
		addDogGraph(navController)
		// imagine 30 more of these in here - that's better, right?}}private fun NavGraphBuilder.addFeedGraph(navController: NavController) {
    composable(route = Screen.Feed) {
	    FeedScreen(
		    showAdoptionPage = { dogId ->
			    navController.navigate(Screen.Adopt.createRoute(dogId))
			}
		)
	}
}

private fun NavGraphBuilder.addDogGraph(navController: NavController) {
    	navigation(route = Screen.Dog, startDestination = Screen.Dog.Adopt) {
		composable(route = Screen.Dog.Adopt) { backStackEntry ->
			valdogId = backStackEntry.arguments? .getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionScreen(
				dogId,
				navigateUp = { navController.popBackStack(DogScreen.Adopt, inclusive = true) }
			)
		}
		composable(route = Screen.Dog.ContactDetails) { backStackEntry ->
			valdogId = backStackEntry.arguments? .getString("dogId")
			requireNotNull(dogId) { "dogId parameter wasn't found. Please make sure it's set!" }
			AdoptionContactDetailsScreen(dogId)
		}
	}
}
Copy the code

As our navigation charts grow, these can also be hosted in their respective files to make things easier to deal with. If your application is modular, this also enables you to encapsulate the navigation and routing of that module within a module. For our example, this means exposing the addFeedGraph extension function for the Dog module. To navigate outside of the nested graph, addFeedGraph also accepts a lambda to navigate to the adoption screen.

If you’re interested in modularity, I highly recommend Joe Birch’s article on modular navigation in Compose!

Lessons from the real world

At Snapp Mobile, we are currently working with a technologically advanced customer who has asked us to deploy Jetpack Compose in a greenfield project. We’ve been using Compose and navigation-compose for a few months now, and from our experience, following the best practices mentioned above is the most important.

Before reading the official advice on decoupling the composables from the NavController, we passed it to all of our screen level composables and established a tight coupling between the composables and the navigation library. Starting with Fragments (sorry, Jake) and Activities from the navigation library, we used to call findNavController from Fragments because we didn’t have any abstract methods before. It was the “natural” way to pass navControllers at first, but as the code base and navigation diagrams grew, it became clear that this led to messy code that was hard to change — and we’re still working to undo that damage.

If you’re just starting out with Navigation-compose, follow this best practice. If you’ve been using it for a while, I suggest you start thinking about how to refactor this code as quickly as possible to reduce the number of things you have to update. Chris Banes recently reconfigured the navigation of his TiVi application to put navigation calls together, if you’re looking for inspiration.

Defining routes and navigation maps correctly and separating them is another important point. As noted earlier, we defined our routes in (nested) objects from the start, which became very difficult to manage and not the most pleasant thing to do later in the refactoring. In the latest Kotlin release, taking advantage of sealed classes and their loose rules is important to keep things in perspective. Considering an application with more than 40 navigation destinations, defining all the routes in one place can end up being a fairly large, difficult to read and unmaintainable file.

But what about transition?

Starting with Navigation 2.4.0-alpha02, transitions between composable destinations are not yet supported. We don’t need it in our current project (yet), but if conversion between destinations is a requirement, it’s a good idea to keep this in mind when you decide to use navigation composition in your project. However, the combo animation and navigation teams are working on this issue and we should see something out after Navigation 2.4.0 stabilizes. In the meantime, I suggest tracking the issue.

Aside from these three issues, while we do occasionally encounter a bug or two, navigation-composition works very well and we are using it with its Hilt integration and are happy with it. Like all new things, we are still in the process of figuring out new best practices, but are happy with our current approach. Navigation 2.4.0 also supports multiple backstacks and fixes a lot of other bugs, so it’s great to see the team actively working on the required features!

resources

You have come to the end! Congratulations to you! Although some things are similar, take a look at the official Android developer documentation for navigation-composition. Chris’s pull request in Tivi is a good example of how we can migrate from a bad navigation mode. If you’re looking for some inspiration, the Tivi library itself is also a good reference for using navigation-compose in real world applications.

Official navigation samples are also a good starting point, but keep in mind that they are just that: samples. They may not follow all best practices (such as not passing your NavController), so be careful not to blindly copy them.

In addition to the official resources, there are some cool community projects for navigating in Compose. Although we don’t use these items in production, I’ve heard others say they like these libraries. It’s definitely worth checking out Zsolt Kocsi’s Compos-Router, Zach Klippenstein’s Compos-Backstack and Arkadii Ivanov’s Decompose library.

Have you ever used navigation-compose? I’d love to hear about your practical experience!

Thanks to Volodymyr Galandzij, mark Dixon, ashdavies â„¢ and Ian Lake for their lovely suggestions and comments!


www.deepl.com translation