Jetpack Compose Clean Navigation | by Igor Stevanovic

Implementing clean navigation in Jetpack Compose

The Jetpack Compose logo used in this image is the official logo created by Google

Everybody in the Android world knows that navigation in Jetpack Compose isn’t its brightest side. There are a lot of callbacks and 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 Screen and ViewModel.

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 and fullRoute. The 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.

Invoking 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 Destination.

Same thing with composable instead of route: String we have destination: Destination.

Now let’s implement that custom navigator. Here’s the code:

AppNavigator has 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 navController functions.

Implementation of AppNavigator is pretty straightforward. Just sending NavigationIntents to the navigationChannel.

One quick note here, I used Dagger-Hilt as a DI framework. Feel free to use any DI framework.

Now let’s implement MainScreen:

In the MainScreen we use our custom NavHost and composable. We remember navController that we pass to NavigationEffects, along with navigationChannel from MainViewModel. 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 navController.

MainViewModel is simple. Just getting navigationChannel from AppNavigator.

The only thing that is left to show, is how we call navigator functions. Let’s take a look at the example of HomeViewModel.

HomeScreen will call corresponding functions and in HomeViewModel we just call AppNavigator functions and as an argument for the routes we invoke Destination.

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 ViewModel.

You can find all of the source code in my GitHub repo.

Want to Connect?GitHub

Portfolio website

News Credit

%d bloggers like this: