Navigating with Animations in Jetpack Compose

Simplified Navigation in Jetpack Compose

Kaustubh Patange
7 min readJun 25, 2021
Yup, straightaway stealing Zhuinden’s style

I bet most of the readers came here thinking about how can we achieve this using the official Navigation Component, but believe me the issue is currently being tracked on the issue tracker for a very long time (Edit: now possible using Accompanist).

This article is not another Navigation Component tutorial (though the title might seem clickbait), here I will show you (or make you aware of) my new navigation library which is an implementation of navigator for Jetpack Compose to handle navigation along with many features (including support for animations).

Why we need another navigation library?

I’m not going to list down the pros-cons of the official navigation library instead I’ll point out something that I don’t like. Remember, these are just my opinions… in the end, you are free to choose whatever suits your needs.

First of all, I want to say that the way of writing & representing UI/Screen using Jetpack Compose is completely different than the traditional way so any knowledge of handling navigation may not be carried over to this new system. Having said that, the way the official Navigation library handles passing of arguments to the other composable screens is not ideal (a lot of unwanted things has to be done just to pass a String), this is because of how it represents a destination (i.e using a String which is also the reason that it has support for handling deeplinks). Naturally, we can create a class to store all their names but when it comes to passing an argument to that destination it would then become something like controller.navigate(“${destinations.profile}/$argument”) which could easily become a source of bug if not defined carefully. Animations are a WIP so there is nothing to talk about (Edit: they are now possible using Accompanist).

Navigation was never a problem for Jetpack compose, by using some combination of remember ,mutableStateOf anyone can pretty much define a basic navigation system. What is challenging is how should we manage the backstack? That’s why there are libraries like compose-router, simple-stack which does that for you, so why another one? The answer is to facilitate navigation so that developers can focus on other important things, that's the one thing I like about the official navigation library is how simple is it to configure & use.

Introducing Navigator Compose

This library is my take on how navigation should be done in Jetpack Compose. So in this article, I’ll give you a simple introduction about my navigation library, what problem does it solve? And most importantly how to use it for complex/deeply nested screens as well. If you are interested & want to read more, check the Github project.

All the code snippets which are shown below are taken from the sample app & documentation (wiki) available on Github.

The first thing we need is to add the required navigator-compose dependency.

Now we need to initialize the ComposeNavigator which is the root class for managing all the navigation related to @Composable screens inside an Activity (no Fragments are required at all).

Once initialized, the ComposeNavigator will attach itself to the lifecycle of the activity which will ensure that the navigation states will be properly restored across configuration & process death.

This setup will also handle the backpress logic on the activity to go up the navigation stack (i.e backward navigation). ComposeNavigator provide APIs for handling backstack like goBack() ,canGoBack() so if you want some custom behavior for handling backpress on the Activity you can use them.

Define a Route

Destinations can be represented through inheritance using a sealed class.

The constructor parameters become the arguments for that destination screen. Note, only data types that can be stored in a bundle are supported.

The MainRoute must implement com.kpstv.navigation.compose.Route interface which internally implements Parcelable to ensure that the state of navigation will persist across configuration change.

We also have declared a key variable in the companion object of the MainRoute (used later during the Setup). This key is used to uniquely identify the Route in the composition tree. This is how findController<T>(key) was able to find & retrieve the Controller associated with that specified key.

From the above snippet, you can notice that we have declared a private noArg parameter for theSecond screen. That’s because we should not use object or simple class to represent an empty argument destination because SaveStateProvider does not restore rememberSaveable’s value after process death (there is a lint-check that will fail the build if such usages are found). Read more about this issue here.

Setup Navigation in MainScreen

The key argument for Setup becomes the key for that navigation which is also used to identify the backstack associated with it.

Navigation to other destinations are managed within the respective @Composable screens. To go back programmatically you can call controller.goBack(). It also provides an API controller.getAllHistory() to get the current snapshot of the history associated with the current key.

Each Setup provides a unique instance of Controller<T> for managing that specific destination type & can be accessed by using findController<T>() within the child composables.

A when statement is used to switch between different @Composable screens based on the target destination dest.

Customizing Navigation with NavOptions

Navigator allows you to customize NavOptions while navigating to other screens. This includes setting enter & exit animations, singleTop or a custom popUpTo logic. More information is available in the documentation.

Below is a small snippet on “navigating to another screen with animations”. There are also some other animations like Slide, Shrink which the library provides out of the box.

Nested Navigation

When you call navigator.Setup, it binds the current ComposeNavigator to the CompositionLocal which can be retrieved using findComposeNavigator(). It also binds the Controller<T> associated with the destination T for all the child composables which can be retrieved using findController<T>().

All you have to do is implement navigator.Setup for another screen where you want nested-navigation.

Check out the Basic Sample for a more clear example.

Support for Dialogs

When you set up navigation with navigator.Setup, the controller that is used to manage the navigation for that Route is used to create, show & close dialogs.

Each dialog must extend from com.kpstv.navigation.compose.DialogRoute, they can be a data class where the constructor parameters becomes the argument for the dialog destination or object for no argument dialog destination.

What’s next?

As you can see from the examples, there is no special magic happening inside the library. Everything is straightforward which it should be!

Animations are there, arguments are properly being passed through destination constructor parameters. What it can’t do yet is handle deeplinks which the official navigation library supports.

Some people may ask, how can we set up Bottom Navigation with this library? This is fairly simple, like said all you have to do is implement navigator.Setup for that screen to act like a nested-navigation. If you take the implementation from the offical navigation library, you just have to replace NavHost with navigator.Setup & that’s it. The sample app showcases some different setup & not the material’s BottomNavigation.

The library is in alpha (not that some features don’t work but the name of some APIs may be changed in the future versions) so you can try it out or check out the official sample app. Also if you have any issues or feature requests just let me know.

Bonus: My thoughts on accompanist’s navigation-animation

So it seems like transitions between composable are now possible using the accompanist’s navigation-animation artifact. It’s been over a month since there are no changes in the library in terms of API design. So, here are some of my opinions & things I dislike.

No issue the library is great & works out of the box by just replacing standard NavHost with AnimatedNavHost which then allows you to specify enter, exit & popEnter, popExit transitions on composable (extension functions on NavHost that represents destinations). This was a deal-breaker for me, what if I want to change the transition depending on the initial & target destination? Since these enter, exit, etc. transitions provide initial, target destination as parameters one can easily wrap the logic in if,else or when but is it worth it? Look at all the code smell with those conditional switches (something like below).

composable("Route1", 
enterTransition = { initial, target ->
when(intial) {
"Route2" -> ...
"Route3" -> ...
"Route4" ->
...
}
},
exitTransition = { initial, target ->
when(target) {
"RouteN" -> ...
...
}
}
) {
// your composable content
}

Suppose I need to add animation for a new destination for “Route1” I would then have to come back to this composable & edit these transitions. This will eventually get out of hand if not properly managed. With my navigator-compose it just becomes an optional argument of NavOptions (see here) & this is for me the idiomatic way as the logic for animation becomes the part of navigation so we can customize it to the full extent.

// with navigator-composecontroller.navigateTo("Route1") {
withAnimation {
target = SlideRight
current = Fade
}
}

Another thing is with the popEnter& popExit, I just don’t see its need in Compose world. Yes, for the fragments it was needed because the backstack was entirely managed by FragmentManager internally (we cannot manually edit it) plus even though Fragments are nothing but a view in a container it has its own lifecycle & is itself a lifecycleOwner so changing states internally might trigger some different behavior especially for popping (more specifically the order in which they are popped).

In Compose, everything is a function & are render based on state changes due to which navigation is predictable. So, with navigator-compose it is basically just a navigateTo call with optional popUpTo DSL (see here) along with withAnimation for customizing animations.

I think the problem is not with the accompanist’s navigation-animation but the way Navigation Component for Compose was designed. Anyway, these are just my opinions & you are always free to use whatever satisfies your needs.

--

--