Compose Destinations - Navigation Library

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

ยท

3 min read

Compose Destinations - Navigation Library

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' 
}

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")
            }
        }
    }
}

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'   
}

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( ... ) { 
    ...
}

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

@RootNavGraph(start = true)
@Destination
@Composable
fun LoginScreen(
    ...
) {
    ...
}

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
) {
    ...
}

Now, it can be replaced with DestinationsNavigator parameter.

fun LoginScreen( 
    navigator: DestinationsNavigator 
) { 
    ... 
}

To navigate, the original implementation use NavHostController

navController.navigate(NavRoute.Home.path)

and now is replaced with DestinationsNavigator

navigator.navigate(HomeScreenDestination)

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} 
}

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) 
    }
}

with DestinationsNavHost()

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

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)
       }
    }
}

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

Did you find this article valuable?

Support Vincent Tsen by becoming a sponsor. Any amount is appreciated!

ย