DEV Community

Cover image for Simple Jetpack Compose Navigation Example
Vincent Tsen
Vincent Tsen

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

Simple Jetpack Compose Navigation Example

Simple app to show different screens in Jetpack Compose and how to navigate between them.

This article was originally published at vtsen.hashnode.dev on April 23, 2022.

I created this simple app to try out the navigation component in Jetpack Compose. This is how the app looks like.

Simple_Jetpack_Compose_Navigation_Example_01.gif

These are the steps to implement this simple app.

1. Add Navigation Compose Library

In app\build.gradle, add this dependency.

dependencies {
    implementation "androidx.navigation:navigation-compose:2.5.0-alpha01"
}
Enter fullscreen mode Exit fullscreen mode

2. Create NavHostController

NavHostController is required to build the navigation graph in the next step which is used to navigate to different screens.

Create this NavHostController using rememberNavController() in your root composable function and pass that into the BuildNavGraph() composable function.

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

3. Build Navigation Graph

The navigation graph looks like this:

  • Login Screen -> Home Screen -> Profile Screen
  • Login Screen -> Home Screen -> Search Screen

Login screen is the start destination. Home screen takes no navigation argument. Profile screen takes 2 navigation arguments and search screen takes 1 navigation argument.

To build the navigation graph, you use NavHost() composable function and NavGraphBuilder.composable() function to build each composable screen.

fun BuildNavGraph(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = "login"
    ) {
        composable(route = "login") {
            //call LoginScreen composable function here
        }

        composable(route = "home") {
            //call HomeScreen composable function here
        }
        ...
    }
}

Enter fullscreen mode Exit fullscreen mode

To navigate to home screen:

navController.navigate("home")
Enter fullscreen mode Exit fullscreen mode

To pop back current stack:

navController.popBackStack()
Enter fullscreen mode Exit fullscreen mode

To pop up to login screen:

navController.popBackStack(NavRoute.Login.path, inclusive = false)
Enter fullscreen mode Exit fullscreen mode

Navigation With Arguments

navGraphBuilder.composable has 2 parameters - route and arguments. To navigation with argument, we want to update both route and arguments parameters.

This is route format for profile screen. id is the first parameter. showDetails is the second parameter.

route = "profile/{id}/{showDetails}"
Enter fullscreen mode Exit fullscreen mode

The arguments parameter looks like this:

arguments = listOf(
    navArgument("id") {
        type = NavType.IntType
    }
    ,
    navArgument("showDetails") {
        type = NavType.BoolType
    }
)
Enter fullscreen mode Exit fullscreen mode

You can specify the NavType of the parameter. You can also set the defaultValue to make the argument optional.

...
    navArgument("showDetails") {
        type = NavType.BoolType
        defaultValue = false
    }
...
Enter fullscreen mode Exit fullscreen mode

I personally will avoid using defautValue because it requires your route to follow certain format (i.e. "profile/{id}/?showDetails={showDetails}"). ?showDetails is the optional arguement to allow you specify the defaultValue.

To retrieve the argument value, you use the NavBackStackEntry.arguments:

composable(
   ...
) { navBackStackEntry ->

    val args = navBackStackEntry.arguments

    // get id param value
    val id = args?.getInt("id")!!
    // get showDetails param value
    val showDetails = args?.getBoolean("showDetails")!!

    // call profile screen composable function here
    ...
}
Enter fullscreen mode Exit fullscreen mode

This is an example to navigate to profile screen.

val id = 7
val showDetails = true
navController.navigate("profile/$id/$showDetails")
Enter fullscreen mode Exit fullscreen mode

Any Route Format Is Fine!

Instead of using this (which I prefer because it is the simplest form):

route = "profile/{id}/{showDetails}"
Enter fullscreen mode Exit fullscreen mode

You can use this (required if you want showDetails to be optional):

route = "profile/{id}/?showDetails={showDetails}"
Enter fullscreen mode Exit fullscreen mode

Or this (required if you want both id and showDetails to be optional): :

route = "profile/?id={id}/?showDetails={showDetails}"
Enter fullscreen mode Exit fullscreen mode

But please do NOT use this:

route = "profile/{id}{showDetails}"
Enter fullscreen mode Exit fullscreen mode

Please make sure you at least put a separator (any string) between the navigation parameters. This navigation parameters could be parsed wrongly especially they're same data type.

If you change the route format, you need to update the navigation call too. For example:

val id = 7 
val showDetails = true
navController.navigate("profile/$id/?showDetails=$showDetails")
Enter fullscreen mode Exit fullscreen mode

Too Much Boilerplate Code

You may have noticed, the hard-coded strings are everywhere. One mistake can just crash the app. It is prone to errors.

So what I did is to create this NavRoute class that has all the hard-coded strings there. I also include a utility functions (i.e. withArgs() to build the navigation path and withArgsFormat() to build the route format string.

sealed class NavRoute(val path: String) {

    object Login: NavRoute("login")

    object Home: NavRoute("home")

    object Profile: NavRoute("profile") {
        val id = "id"
        val showDetails = "showDetails"
    }

    object Search: NavRoute("search") {
        val query = "query"
    }

    // build navigation path (for screen navigation)
    fun withArgs(vararg args: String): String {
        return buildString {
            append(path)
            args.forEach{ arg ->
                append("/$arg")
            }
        }
    }

    // build and setup route format (in navigation graph)
    fun withArgsFormat(vararg args: String) : String {
        return buildString {
            append(path)
            args.forEach{ arg ->
                append("/{$arg}")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Some usage examples:

// navigate to home
navController.navigate(NavRoute.Home.path)

// navigate to search
navController.navigate(NavRoute.Search.withArgs(query))

// setup route for search screen with query param
route = NavRoute.Search.withArgsFormat(NavRoute.Search.query)
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

I'm not sure my NavRoute approach above is the good one. I'm also not sure if it makes the code unreadable? Maybe a bit? However, it at least can get rid of many hard-coded strings and no more boilerplate code.

There is a Compose Destinations library which removes even more boilerplate code. I think it is better to understand the fundamental first before trying out this library.

Here are the steps to convert this app to use this library:

Source Code

GitHub Repository: Demo_SimpleNavigationCompose

See Also

Discussion (0)