DEV Community

Cover image for Compose Destinations - Navigation Library
Vincent Tsen
Vincent Tsen

Posted on • Edited on • Originally published at vtsen.hashnode.dev

Compose Destinations - Navigation Library

How to convert your Jetpack Compose navigation app to use Compose Destinations Library to get rid of boilerplate code?

This is part of the Jetpack Compose navigation series:

In my previous post, I build a very simple Jetpack Compose Navigation and use the NavRoute sealed class to avoid hard coding strings in multiple places.

However, a better solution may be just using this awesome Compose Destinations library! Let's see how we can convert this app to use this library.

Setup build.gradle (module level)

1. Add KSP Plugin

Add com.google.devtools.ksp plugin

plugins {
    ...
    id 'com.google.devtools.ksp' version '1.6.10-1.0.2'
}
Enter fullscreen mode Exit fullscreen mode

2. Add Generated KSP Path

Add generated KSP path inside the android block

android {
    ...
    applicationVariants.all { variant ->
        kotlin.sourceSets {
            getByName(variant.name) {
                kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin")
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

3. Add Compose Destination Dependencies

dependencies {
    ...
    implementation 'io.github.raamcosta.compose-destinations:core:1.5.1-beta'
    ksp 'io.github.raamcosta.compose-destinations:ksp:1.5.1-beta'   
} 
Enter fullscreen mode Exit fullscreen mode

For build.gradle.kts example, you can refer to this commit here.

Build Navigation Graph

Existing navigation graph related code (i.e. BuildNavGraph() and NavRoute) code can be removed completely and replaced with compose destinations annotations.

1. Annotate Screens with @Destination

Annotate all composable screens with @Destination

@Destination
@Composable
fun LoginScreen(
   ...
) {
...

@Destination
@Composable
fun HomeScreen(
   ...
) {
...
@Destination
@Composable
fun ProfileScreen(
   ...
) {
...
@Destination
@Composable
fun SearchScreen(
   ...
) {
...
Enter fullscreen mode Exit fullscreen mode

2. Annotate Start Screen with @RootNavGraph(start = true)

@RootNavGraph(start = true)
@Destination
@Composable
fun LoginScreen(
   ...
) 
...
Enter fullscreen mode Exit fullscreen mode

After you annotate the composable screen, make sure you Rebuild Project so all the necessary generated code will be generated.

3. Replace NavHostController with DestinationsNavigator

In the original login composable screen, it has this navigateToHome callback.

fun LoginScreen(
    navigateToHome: () -> Unit
) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Now, it can be replaced with DestinationsNavigator parameter.

fun LoginScreen(
    navigator: DestinationsNavigator
) {
   ...
}
Enter fullscreen mode Exit fullscreen mode

To navigate, the original implementation use NavHostController

navController.navigate(NavRoute.Home.path)
Enter fullscreen mode Exit fullscreen mode

and now is replaced with DestinationsNavigator

navigator.navigate(HomeScreenDestination)
Enter fullscreen mode Exit fullscreen mode

HomeScreenDestination is the generated code.

Some other conversion examples below

// #1  - popBackStack() 
// convert NavHostController
navController.popBackStack()
// to DestinationsNavigator
navigator.popBackStack()

// #2 - navigate with arguments
// convert NavHostController
navController.navigate(NavRoute.Profile.withArgs(id.toString(), showDetails.toString()))
// to DestinationsNavigator
navigator.navigate(ProfileScreenDestination(7, true))

// #3  - popUpTo() 
// convert NavHostController
navController.navigate(NavRoute.Login.path) {
        popUpTo(NavRoute.Login.path) {inclusive = true}
}
// to DestinationsNavigator
navigator.navigate(LoginScreenDestination) {
   popUpTo(LoginScreenDestination.route) {inclusive = true}
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the DestinationsNavigator is basically a wrapper for NavHostController which makes it a lot easier.

4. Call DestinationsNavHost() in the main composable screen

Replace BuildNavGraph()

@Composable
private fun MainScreen() {
    SimpleNavComposeAppTheme {
        val navController = rememberNavController()
        BuildNavGraph(navController)
    }
}
Enter fullscreen mode Exit fullscreen mode

with DestinationsNavHost()

@Composable
private fun MainScreen() {
    SimpleNavComposeAppTheme {
        DestinationsNavHost(navGraph = NavGraphs.root)
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Use EmptyDestinationsNavigator in @Preview

Thanks to the author of this library, Rafael Costa told me that I can actually use EmptyDestinationsNavigator as null implementation and used it for @preview instead of passing innull.

Instead of passing in navigator = null, I can pass in navigator = EmptyDestinationsNavigator.

@Preview(showBackground = true)
@Composable
private fun DefaultPreview() {
    SimpleNavComposeAppTheme(useSystemUiController = false) {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colors.background
        ) {     
            HomeScreen(navigator = EmptyDestinationsNavigator)
       }
    }
}
Enter fullscreen mode Exit fullscreen mode

By doing that, I don't need to declare navigator: DestinationsNavigator? nullable variable parameter in the composable function.

Done!

Conclusion

This library is awesome! It gets rid of much boilerplate code. One thing I wish is I don't need to set up as in Step 1 - Add KSP Plugin and Step 2 - Add Generated KSP Path above, but maybe that is not technically feasible.

Source Code


See Also


Originally published at https://vtsen.hashnode.dev.

Top comments (3)

Collapse
 
moheltanani profile image
Mohamed Eltanani

Great!
I tried to use the library, the only issue I have is, when I use it to navigate to next screen, scaffold gets hidden like top and bottom nav bars they will be gone!

Any idea?

Collapse
 
vtsen profile image
Vincent Tsen

I think you need to manually pass in the NavHostController to your custom Top/Bottom bar composable function. Then, use it to decide whether you want to show them or not based on the specify route destination. Something like this:

val destination = navController
    .appCurrentDestinationAsState().value

if(destination == XxxScreenDestination) {
    //Show your top bar here
}
Enter fullscreen mode Exit fullscreen mode

You can create NavHostController like this and pass it in as a parameter into your top/bottom bar and also the DestinationsNavHost()

val engine = rememberNavHostEngine()
val navController = engine.rememberNavController()
Enter fullscreen mode Exit fullscreen mode
Collapse
 
techmarinar profile image
TechMarinar

thanx man , this was awesome :)