DEV Community

Cover image for Using compose destinations
aseem wangoo
aseem wangoo

Posted on

Using compose destinations

In case it helped :)
Pass Me A Coffee!!

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()
Enter fullscreen mode Exit fullscreen mode

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
navController.navigate("screen_route")
// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
    popUpTo("home")
}

// 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 }
}
Enter fullscreen mode Exit fullscreen mode

Passing custom arguments

navController.navigate(Destinations.Home.replace("{user}",userJson))

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")
    // DO YOUR PROCESSING AND CONVERT TO USER OBJECT
    HomeView(navController, userModel = userObject!!)
}
Enter fullscreen mode Exit fullscreen mode

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

Setup

  • 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'
Enter fullscreen mode Exit fullscreen mode
  • Add the following inside the plugin of build.gradle
plugins {
    id 'com.google.devtools.ksp' version '1.6.10-1.0.2'
}
Enter fullscreen mode Exit fullscreen mode

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 {
            kotlin.srcDir("build/generated/ksp/debug/kotlin")
        }
        release {
            kotlin.srcDir("build/generated/ksp/release/kotlin")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
@Destination
@Composable
fun AddTodoView(navController: DestinationsNavigator) {}
Enter fullscreen mode Exit fullscreen mode
  • Let’s run the command below which generates all the Destinations
./gradlew clean build
Enter fullscreen mode Exit fullscreen mode

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?) {
  super.onCreate(savedInstanceState)
  setContent {
      AppTheme {
          DestinationsNavHost(navGraph = NavGraphs.root)
      }
  }
 }
}
Enter fullscreen mode Exit fullscreen mode
  • 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
navController.navigate(AddTodoViewDestination)

// 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 }
}
Enter fullscreen mode Exit fullscreen mode

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.

@Parcelize
data class GoogleUserModel(
    val name: String?,
    val email: String?
) : Parcelable
Enter fullscreen mode Exit fullscreen mode
  • We modify our Home composable as below
@Destination
@Composable
fun HomeView(
    navController: DestinationsNavigator,
    userModel: GoogleUserModel,
) {}
Enter fullscreen mode Exit fullscreen mode

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
navController.navigate(
    HomeViewDestination(
        GoogleUserModel(
            email = user.email,
            name = user.name,
        )
    )
) {
    popUpTo(route = AuthScreenDestination.routeId) {
        inclusive = true
    }
}
Enter fullscreen mode Exit fullscreen mode

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

DestinationsNavigatorImpl
DestinationsNavigatorImpl

// PREVIOUS
private lateinit var navController: TestNavHostController
// NOW
private var navController = DestinationsNavigatorImpl()
Enter fullscreen mode Exit fullscreen mode

We replace NavigationComponentwith DestinationsNavHost inside our tests

composeTestRule.setContent {

     // PREVIOUS 
     NavigationComponent()

     // NOW
     DestinationsNavHost(navGraph = NavGraphs.root)
 }
Enter fullscreen mode Exit fullscreen mode

Source code.

In case it helped :)
Pass Me A Coffee!!

Top comments (0)