We will cover briefly:

  1. Current navigation in compose
  2. Using compose destinations
  3. (Optional) Modify existing test cases

Current navigation in compose

We get Compose Navigation from the Jetpack Compose. It provides a Navigation component that allows you to navigate between different composable.

Let’s see it in action using our code example.

Navigation Component
Navigation Component
  • We have our NavigationComponent which is composable. You can assume this file contains all the routes in our app. As we can see, there are 3 routes here namely : Auth Home and AddTodo
  • The starting or the first route is the Auth and based on some logic(Sign in Google logic in our case) it redirects to the Home while passing an object user
  • All our routes need a Navigation Controller. The NavController is the central API for the Navigation component. It keeps track of the back stack of the screens in your app and the state of each screen.
  • We create a NavController by using the rememberNavController() method and pass it in our Views
val navController = rememberNavController()

Creating NavHost

  • Each NavController must be associated with a NavHost composable. The NavHost links the NavController with a navigation graph that specifies the composable destinations that you should be able to navigate between.
  • In order to tie this everything, we place the above NavigationComponent inside our main activity
Main Activity
Main Activity
  • For navigating a route, we use the navigate() method. navigate() takes a single String parameter that represents the destination’s route.
// Navigate to some screen
// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {

// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
    popUpTo("home") { inclusive = true }

Passing custom arguments


By default, all arguments are parsed as strings. Next, you should extract the NavArguments from the NavBackStackEntry that is available in the lambda of the composable() function.

composable(Destinations.Home) { backStackEntry ->
    val userJson = backStackEntry.arguments?.getString("user")
    HomeView(navController, userModel = userObject!!)

If you are still reading, you may have realized is it too much work? And I agree with you. There is some boilerplate code, especially in the case of passing the custom arguments. Well, can this be improved? Yes!!!

Using compose destinations

Forget about the previous section, and start afresh. Assume you need to implement navigation inside your app.

Introducing Compose Destinations. As per the documentation

Compose Destination: A KSP library that processes annotations and generates code that uses Official Jetpack Compose Navigation under the hood. It hides the complex, non-type-safe and boilerplate code you would have to write otherwise. Most APIs are either the same as with the Jetpack Components or inspired by them.

Compose Destination
Compose Destination


  • Install the dependencies inside build.gradle of your app
ksp 'io.github.raamcosta.compose-destinations:ksp:1.4.2-beta'
implementation 'io.github.raamcosta.compose-destinations:core:1.4.2-beta'
  • Add the following inside the plugin of build.gradle
plugins {
    id 'com.google.devtools.ksp' version '1.6.10-1.0.2'

Note: Compose Destinations takes advantage of annotation processing (using KSP) to improve the usability of Compose Navigation.

  • Include a kotlin block that defines the sourceSets for the generated code inside your build.gradle
kotlin {
    sourceSets {
        debug {
        release {

Implement Navigation

So you still have the routes : Auth Home and AddTodo Now let’s see how to implement navigation. You have your existing composables. Let’s annotate them

Compose Destination Annotation
Compose Destination Annotation
  • We use the Destination annotation which comes from the Compose Destinations
  • We mark our AuthScreen with start = True which implies this destination is the start destination of the navigation graph
  • Next, we change the type of our navController to DestinationsNavigator

DestinationsNavigator is a wrapper interface to NavController.

  • For the other screens, let’s say AddTodo, we simply annotate it
fun AddTodoView(navController: DestinationsNavigator) {}
  • Let’s run the command below which generates all the Destinations
./gradlew clean build

If the result is a success, you should see the generated code inside the build/generated/ksp/debug/kotlin

Generated Destinations using Compose Destinations
Generated Destinations using Compose Destinations

Using NavHost

In case you realized, we no longer need the NavigationComponent class.

  • One final thing remaining is to add the NavHost inside our MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
  setContent {
      AppTheme {
          DestinationsNavHost(navGraph = NavGraphs.root)
  • Our DestinationsNavHost includes the destinations of navGraph. It includes all the composables annotated with Destination inside NavGraphs generated file.
  • NavGraphs is a generated file that describes your navigation graphs and their destinations. By default, all destinations belong to the NavGraphs.root
  • Finally, for navigating to screens we still follow the same convention, but now we have the Destinations (like AddTodoViewDestination etc) mapped to each of the views.
// Navigate to add view screen

// Pop everything up to and including the "auth" destination off
// the back stack before navigating to the "Home" destination
navController.navigate(HomeViewDestination) {
    popUpTo(AuthScreenDestination.routeId) { inclusive = true }

Passing custom arguments

Here’s why this gets interesting. Let’s see a case, for example, we have an Auth screen and we need to pass the user object to our Home screen.

data class GoogleUserModel(
    val name: String?,
    val email: String?
) : Parcelable
  • We modify our Home composable as below
fun HomeView(
    navController: DestinationsNavigator,
    userModel: GoogleUserModel,
) {}

We add the parcelable classGoogleUserModel to the parameters and next, we again need to run our build command which updates the generated destination for the HomeView

  • Inside our AuthView once we get the required data from the API, we create the GoogleUserModel and pass it to the HomeViewDestination
            email = user.email,
            name = user.name,
) {
    popUpTo(route = AuthScreenDestination.routeId) {
        inclusive = true

After we navigate to HomeView we pop the routes, including AuthView by specifying the AuthScreenDestination inside the popUpTo.

Modify existing test cases

We created some tests based on the NavigationComponent (when we were using it) but since we longer have it with us, we make use of the DestinationsNavigator

  • Since DestinationsNavigator is an interface, we create our own DestinationsNavigatorImpl the class.
  • Our DestinationsNavigatorImpl simply extends from the DestinationsNavigator class and we override all the methods from the DestinationsNavigator
private lateinit var navController: TestNavHostController
// NOW
private var navController = DestinationsNavigatorImpl()

We replace NavigationComponentwith DestinationsNavHost inside our tests

composeTestRule.setContent {
     // PREVIOUS 
     // NOW
     DestinationsNavHost(navGraph = NavGraphs.root)
Source code

Valuable comments