Implementing clean navigation in Jetpack Compose
Everybody in the Android world knows that navigation in Jetpack Compose isn’t its brightest side. There are a lot of
navControllers that you need to pass to execute navigation, and what if you need to do some business logic before sending arguments? Code gets pretty messy.
There are a lot of discussions on how to implement navigation, and one great library also. Feel free to check it out, shout out to Rafael Costa for creating something like that.
But what if you don’t want to be dependent on somebody else, or the policy of your company is to not use external libraries? Navigation is one of the most important things in the app. You can’t rely on someone else, even tho that library is maintained 24/7. You need to create a solution, that works well. Before going through my clean solution, let’s dive into the problem so we understand 100% what we are doing here.
The current most common implementation of navigation is this:
You pass lambdas, and that’s it. The second option is that instead of lambdas you pass
navController. Either solution doesn’t look that good. The screen could have too many callbacks if it is complex enough. Your code gets messy a lot. Maybe your logic around routes isn’t hardcoded like this, but you get the point of the problem.
What if you need to do some business logic, for example, calculate something, and the result of the calculation is an argument for the next screen? You need to call
ViewModel to do the business logic(the
Viewshouldn’t do that), observe the result and then invoke the callback. Too much forwards and backward between the
Let’s try to fix all these problems, and make it a little bit cleaner.
The idea of my solution is to have a custom navigator, that will be provided to every
ViewModel. By calling functions of the navigator we are navigating to different screens. All navigation events are collected in the
MainScreen and in that way we don’t need to pass callbacks or
navController to the other screens. It will be more clear once we go through the code.
First, let’s create a special class for the routes:
Destination has a constructor with two arguments. The first is the base route and the second one is the parameters for that route. Each
Destination will have
route is a base route without parameters, will use that one to create
fullRoute with parameter names or
fullRoute with the value of the parameters.
Destination will return its route.
appendParams function will just add parameters to the route and return
fullRoute with the value of the parameters.
Next is to add some navigation composables we are gonna use.
NavHost is the same as one from
androidx.navigation.compose the only difference is that
startDestination argument is of type
Same thing with
composable instead of
route: String we have
Now let’s implement that custom navigator. Here’s the code:
navigationChannel that will be collected in the
MainScreen and it has four functions for navigating.
NavigationIntent contains all navigation intents that can happen. You can add here more of them, for example, one for deep links or something like that. Arguments of every
NavigationIntent are needed for
AppNavigator is pretty straightforward. Just sending
NavigationIntents to the
One quick note here, I used
Dagger-Hilt as a DI framework. Feel free to use any DI framework.
Now let’s implement
MainScreen we use our custom
composable. We remember
navController that we pass to
NavigationEffects, along with
NavigationEffects just collect the
navigationChannel and navigate to the desired screen. As you can see, it is cleaner and we don’t have to pass any callbacks or
MainViewModel is simple. Just getting
The only thing that is left to show, is how we call navigator functions. Let’s take a look at the example of
HomeScreen will call corresponding functions and in
HomeViewModel we just call
AppNavigator functions and as an argument for the routes we invoke
Take a look at
UsersViewModel to see examples for navigating back and passing parameters to the route.
And that’s it!
I think there is still room for improvement, but this could be a good starting point. It made our code a lot cleaner, and there is no need to collect side effects from
ViewModel in our screens just so we can navigate.
We can argue if the navigation should be done from the
ViewModel but I think it should be. The
View should be “stupid,” and only show data. What we do on click, should be the responsibility of the
You can find all of the source code in my GitHub repo.
Want to Connect?GitHub