
Navigation with Animated Transitions Using Jetpack Compose
This article along with the source code on Github has been deprecated. For the newer article and source code, visit the Jetmagic project on Github:
A sample application that uses a custom navigation library allowing you to navigate screens with animated transitions and pass any data type.

The source code for this app is available at:
https://github.com/JohannBlake/navigation-with-animated-transitions-using-jetpack-compose
The app is about showing a list of cats in a grid, displaying details about each cat and navigating to a fake “adoption” screen. It also features a fancy navigation drawer as well as collapsible/fixed toolbars. It also uses Paging library 3 with Flow to retrieve the data. The focus of this article however is on how the app implements navigation between the various screens.
Limitations of the Compose Navigation Framework
As I am about to undertake developing a new Android app from scratch, I had to decide whether to develop it using the older view base UI system or use Jetpack Compose. At the time of this writing, Compose is still in Beta and it will probably not be until next year when the first non-beta version is released.
I decided to play around with Compose for several weeks to get a feel for whether it was at least at a level sufficient enough to allow me to develop a professional app. Having developed Android apps for the past 10 years, I am very aware that it took Google 10 years to develop a robust and stable View based UI system. So there was no way Compose in beta form was going to come close to the level of maturity that the view system already has. Still, if it could at least provide an adequate amount of stable basic features, it would be worthwhile considering it for a new project.
The biggest change when developing with Compose is that you really are developing a single activity app. Developing a single activity app isn’t anything new but building one with Compose is. The concept of having multiple activities or even fragments no longer exists, although there is nothing stopping you from mixing the old view-based system with Compose. If you are building a new app from scratch, however, it is recommended that you ditch multi-activities as there is no real need for them.
Jetpack Compose is a decision the Android team made based upon a growing trend in how UIs are developed across various platforms. It probably began with Facebook’s React. In a nutshell, a UI framework based on React/Compose (or similar frameworks) treats your app as having only one UI and updating only those parts of the UI that change. All parts of an app’s UI are arranged in a hierarchy. When you update (“recompose”) one part of the hierarchy, it updates that level and all its children beneath it. Generally speaking, “screens” are just sibling composables where only one is visible at a time. Navigating from one screen to another is really just hiding one screen and showing another.
The Jetpack Compose documentation has an API for navigating the screens in your app and it is well documented, so I won’t be repeating anything here about it. Once I started using it though, it became apparent that it had limitations, and I had to decide whether it was worth living with those limitations.
The first limitation I discovered was that you were limited to the data types that you would use when passing data from one screen to another. These were limited to the native types like strings, integers and booleans. You could pass objects but these would have to be converted to parcelables. It was clear to me that the Android team decided to carry over the same old baggage from the view system that was used when passing data to other activities or fragments.
I found this odd. I mean, if you’re going to build an entirely new UI framework, why on earth would you limit yourself to the restrictions that were previously in place? I could understand this in the old days when Android was starting out and Java was the official language and the language constructs may have made it difficult to pass any kind of object, but this is 2021 and Kotlin has been around for several years already — so these restrictions seem very lame.
You may be wondering why I find this limitation important. If you’ve worked on many apps, you will probably realize that passing data models around in your app is quite normal. For example, you make a backend API call to retrieve some data about a hotel room. The data model for this room could be fairly large. On one screen you might just display some summary information about the room but then click on something which brings up more detailed information. Instead of making a second API call to the backend, you may already have the data in the model to display on the details screen. In this scenario, you just want to pass the data model from one screen to another. You could of course just pass all the data that is to be displayed on the details screen as individual parameters using generic data types, but that usually isn’t practical. Using the older view system, some developers will convert the data model to a parcelable and pass it that way.
But with activities and fragments no longer being used in an app developed with Compose, there is no reason to confine your development to the limited data types.
Another limitation I came across with the navigation framework was the lack of support for animated transitions from one screen to another. At the time of this writing, Google has mentioned in the docs that this feature is in the pipeline. Be sure to check out the links to all the sample mobile web apps at the end of this article. They include examples of some very impressive 3D animations. Some of these animations would be difficult to achieve even with the Compose animation framework. But knowing what is possible in web based UI should at least inspire you to achieve a high level of UI/UX design that engages your users.
Using animated transitions may not be an important feature, but it does raise the question as to why it was not part of the navigation framework from the start. After all, the Android team had already invested heavily into the Compose animation framework that does allow you to perform basic and complex animations. For me, this raised a red flag that the Compose navigation framework was probably poorly conceived from the start. Not providing support to pass any data type and not supporting animated transitions made me question whether I really wanted to even use the navigation framework.
As a result, I decided to play around with Compose and see if I could build a better navigation framework that supported any data type and allowed animated transitions. Supporting any data type was very simple. Adding support for animated transitions however was a challenge and took me some time to figure out how Compose actually works with animations. This was something I was not going to learn by reading the Compose docs. The challenge was understanding what Compose was doing under the hood and most of this is not mentioned in the documentation. Once I understood what was going on, creating a solution was actually quite simple.
How Compose Handles Animations
Using animations is thoroughly documented and straightforward. You can make something animate into or out of view by using AnimatedVisibility. This works as expected but only under one very important condition. You have to know in advance what you want to show or hide. In other words, that part of the UI that you want to show or hide has to already be part of the screen hierarchy when Compose composes or recomposes the composable that is hosting your UI widget. By “widget”, I’m not just referring to something like a button or text composable. In Jetpack Compose, virtually everything you show in the app is a “widget”, including things like the Scaffold composable. In reality, there are two types of composables: one that generates UI stuff that can be seen and another that is used as non-UI stuff that acts like infrastructure support within the screen hierarchy.
The problem you need to address when it comes to navigation is that you don’t know in advance where a user is going to navigate to. So at the moment that the user navigates from one screen to another and you want to have an animated transition, the other screen has to already be part of the screen hierarchy before the navigation even begins. This is probably not obvious. In an app without any animated transitions, you just show or hide a composable by including or excluding it based on a boolean state. In this case, the composable is just part of some other composable within the entire screen hierarchy. It’s only when you start using animated transitions that this falls apart. To have a slide-in/out transition, the composable must exist on the same hierarchy level as the screen it is replacing. You accomplish that by either placing the composables for the screen you are replacing and the screen you are navigating to inside a Box or Surface composable. And the screen you are navigating to must be added after the one you’re navigating away from. This means that you cannot really have a screen hierarchy where the screen you are navigating to is located at a lower level in the screen hierarchy than the one you are navigating away from.
A simpler way of stating this, is that if you plan on using animated transitions, all the screens must exist at the same time on the same hierarchy level as siblings.
One solution is to just add all the screens to your app when it starts up and keep them hidden until you need to display one of them. But this is problematic for a few reasons. First, in a large app, I’m quite sure that this would suck up a lot of resources. If a user doesn’t navigate to a particular screen, a hidden screen could potentially be wasting memory. It should be noted here that I am only assuming this. Without knowing how Compose manages hidden screens, it might turn out that it can keep a very small resource footprint. Even if you did know how Compose does this, it is an internal implementation that could change. So assuming that Compose can keep a low resource footprint for hidden composables is kind of risky.
Another problem with adding all the screens to the screen hierarchy when the app starts up is that the hierarchy automatically dictates the Z-order of the composables in the order in which you add them. Those added last (lower down in the hierarchy) appear above (on top) of those that appear higher up in the hierarchy. I’m not sure whether Compose has support for setting the Z-order on composables, but even if it did, I can’t imagine a worse way of managing your UIs. I’m sure it would be very hacky and a pain to manage.
Where you can see a huge difference between two apps in the way they employ navigation is the YouTube and Instagram apps.
The YouTube app is rather primitive compared to the Instagram app. When you click on a video listing, the video plays and you are also presented with a list of additional related videos. If you click on a related video, it simply replaces the current screen with the selected video. You can keep on doing this and might assume that if you hit the Back button you will return to the previous video. Instead, what you will find is that you end up returning to the home screen. If you hit the Back button again, you exit the app. To be honest, this seems rather lame. Users expect to return to the previous screen when they hit the Back button. Another thing is that an animated transition is only used when you click on a video on the home screen. No animations are used when you click on a related video. The screen just reloads quickly. This is a minor point however.
Instagram on the other hand is much cooler. You can click on an image or link on one screen and an animated transition is done to the next one. From there you can continue clicking and navigating to the next screen and so on. There is no limit. Whenever you hit the Back button, it performs an animated transition hiding the current screen. The previous screen is still visible and the state of the screen is retained (including the scroll position). This is the behavior my demo app achieves.
A Better Way To Handle Multiple Screens
I eventually came up with a way that allows you to navigate to any screen using animated transitions without the need to have all the screens in the screen hierarchy pre-loaded when the app starts up.
To accomplish this, each screen gets added to the UI when needed and is added to a single parent which can be either a Box or Surface composable. There is only one instance of a parent in the app. This parent is referred to as the “screen factory” and is responsible for the creation of a new screen whenever the user navigates forward. It is also responsible for recomposing the screen hierarchy whenever the user navigates backward.
Each time the user navigates forward to another screen, the parent composable is updated to recompose all the children composables. All the children screens are siblings and are completely separate from each other. However, because they are part of a Box or Surface, the order in which they get added is important. The last child will always be on top. This of course is what we want. When you navigate forward, you want the last screen in the stack to be on top.
As mentioned previously, animations only work if the screen you are animating already exists in the screen hierarchy. But since we don’t know which screen the user will navigate to, the solution is to fake it in advance. This is done by adding a hidden composable to the parent as the last item. I refer to this hidden screen as the “placeholder screen”. The placeholder screen contains no content other than just a Box or Surface. In the demo app, I chose to use a Surface. Whenever the user wants to navigate forward to another screen, the placeholder screen is recomposed to contain the composable of the screen you want to navigate to. Prior to doing this though, a new placeholder screen is added to the end of the navigation stack, effectively becoming the new placeholder screen, replacing the previous one. The previous placeholder screen is one screen below this in the stack and now refers to the screen to navigate to. At this point, it is just a matter of informing the parent that a new screen needs to be displayed. The parent will then iterate through all the children screens, causing them to recompose. When the last screen (the one the user is navigating towards) is recomposed, the animated transition will work because that screen was previously part of the screen hierarchy as a placeholder.
Navigating backwards only requires informing the last screen to make itself hidden. The last displayed screen is also removed from the navigation stack. The placeholder screen at the end of the stack is always available for navigating forward to another screen. When the last screen closes (hiding itself), the parent composable does not need to recompose itself or any of the children screens it contains. Even though the last displayed screen has removed itself and is now hidden, it still does take up some minimal amount of resources. But as soon as the user navigates to another screen, I assume that this resource will be garbage collected as it will no longer be part of the screen hierarchy.
One thing I should point out here is that when you initialize the navigation manager and define the screens your app will support, you can optionally provide a reference to a viewmodel class. The navigation manager will create the viewmodel whenever the screen needs to be displayed and each screen gets its own viewmodel. Of course, you don’t have to do this. You can manage each viewmodel on your own. But one side benefit I discovered was that because the navigation manager is the single source of truth for the navigation stack as well as the optional viewmodels it associates with a screen, this allows one screen to access the viewmodel of a different screen. Where this might be useful is when you navigate backwards and want to return some data to a previous screen from the current screen. The current screen could access the viewmodel of the previous screen and call a function or property on it and pass in the data.
The following code is used in the app for the screen factory:
The filename is ScreenFactoryUI.kt. It follows the pattern of unidirectional data flow and hoists the data state and event handling to the ScreenFactoryHandler composable. Whenever the user navigates forward, the navigation manager will notify the screen factory with:
NavigationManager.onScreenChange.observeAsState(0).value
This is a LiveData property and its value is some random value between 0 and 1 million. The navigation manager does this to force the LiveData to notify observers. This is a technique I came up with when developing the wirespec.dev website which uses React. Since Compose and similar frameworks like React rely on state changes to know when to recompose their UI, I found that using a randomly generated number is a simple way of guaranteeing that observers will be notified of changes.
Once the screen factory gets the notification, it iterates through all the screens in the navigation stack and composes or recomposes them. All of them except the last one are made to be visible. The last one is the placeholder screen and is kept hidden.
The ScreenFactory composable is responsible for creating the actual screen and providing the navigation to that screen. In the demo app, all screens use the same animation. Only the home screen uses no animation. In your app, you can choose to apply different animated transitions to each screen as you choose. You can have a completely different animated transition for your splash screen if you have one.
Whenever the user navigates back to a previous screen, this line of code gets called:
var closeScreen = navInfo?.onCloseScreen?.observeAsState(false)?.value
The closeScreen property will be set to true. The instance of ScreenFactory associated with the last screen is then responsible for hiding the screen.
It should be noted that whenever the placeholder screen is added, the screen type is set to null for that screen. Only when the user navigates forward to a screen does the screen type get set on the placeholder screen and the placeholder screen ceases being the placeholder screen. A new placeholder screen is added to the end of the stack. That is why this line of code:
if (navInfo?.screen != null) {
ignores the placeholder screen and an empty Surface composable is used for the placeholder.
Summary
The navigation manager is developed as a library. It’s only available in source code form at the moment but is part of the github project under the navigation module. For details on how to initialize the navigation manager and the other API functions and properties it offers, see the Github project.
Even if Google does add support to the Compose navigation framework to handle any data type and animated transitions, you may want to go the route I have chosen as it is simple and very flexible. You have complete control over navigation and won’t have to worry when you need to implement some special way of navigating in your app. Just to give you an idea on the vast number of creative ways an Android app can potentially provide engaging ways of letting users navigate an app, I have compiled a list of mobile web apps that demonstrate the possibilities. Not everything shown in these apps are easily implemented in Android. Although these apps are for demonstration purposes, they are fully functional when you click and scroll the various UI elements. I would suggest you review all of these apps in great detail before you begin designing your next app. They can serve as inspiration and take your app to the next level in the user experience. If you don’t have time to review all of them, check out these ones as they have some really nice UI/UX with great animations (not necessarily animated screen transitions):
https://preview.enableds.com/product/?theme=sidebars3d&round
https://preview.enableds.com/product/?theme=touchbar&round
https://preview.enableds.com/product/?theme=ultramobilepwa&round
https://preview.enableds.com/product/?theme=20kmobile&round
http://preview.themeforest.net/item/eclipse-mobile-template/full_screen_preview/15836565
https://preview.enableds.com/product/?theme=glovebox&round
https://preview.enableds.com/product/?theme=sitebar&round
http://preview.themeforest.net/item/jetpack-mobile-template/full_screen_preview/15354227
If you have time, these are also worth checking out…
https://preview.enableds.com/product/?theme=promobile&round
https://preview.enableds.com/product/?theme=appecapwa&round
https://preview.enableds.com/product/?theme=eazypwa&round
https://preview.enableds.com/product/?theme=materialish&round
https://preview.enableds.com/product/?theme=appkitpwa&round
https://preview.enableds.com/product/?theme=azurespwa&round
https://preview.enableds.com/product/?theme=bemobile&round
https://preview.enableds.com/product/?theme=drawer&round
https://preview.enableds.com/product/?theme=duodrawerpwa&round
https://preview.enableds.com/product/?theme=justmobile&round
https://preview.enableds.com/product/?theme=photroller&round
https://preview.enableds.com/product/?theme=slidebox&round