A comprehensive thirty-line navigation for Jetpack/Multiplatform Compose
Navigation is an important part of almost every application. There is an official (first-party) navigation library for Jetpack Compose provided by Google. But it is only available on Android.
Recently I published two articles that touch on the topic of navigation in Kotlin Multiplatform. The first one is “Decompose — experiments with Kotlin Multiplatform lifecycle-aware components and navigation”, and the second one is “Fully cross-platform Kotlin applications (almost)”. Both articles demonstrate how the Decompose library can be used for more than just sharing navigation logic between different platforms (e.g. Android, iOS, Desktop, JavaScript). It allows us to split Kotlin Multiplatform projects into independent scoped components, with their own life cycle, state preservation, ability to retain instances (aka AndroidX ViewModels), and (most importantly) platform specific pluggable UI (like Compose, SwiftUI, JS React).
But such functionality is not always needed. For example, in a pure Desktop Compose project, one might want to write code using only Composable functions. While I am still convinced that Compose should only be used for the UI and nothing else, other people may still need a solution for navigation in the pure Composable world.
In this article I will demonstrate how Decompose can be used to add full featured navigation in pure Composable world. We will create our own thirty-line Composable navigation API step by step. The API I’m going to suggest is just an example, you can come up with your own or create something similar to Jetpack Compose Navigation.
The complete code and a sample project are available on GitHub.
A simple navigation without any libraries
Let’s step back for a minute and try to implement a very simple navigation without any libraries. Let’s say we have two screens, List and Details. When we select an item in the list we should show its details. And when we click Back button, we should go back to the List screen.
A List screen with 100 items can look like this:
The Details screen with Back button:
To add navigation between them, let’s represent all of the child screens as a sealed class:
This structure describes all possible screens and their arguments.
The Main (parent) Composable can be implemented as follows:
We defined val screenState
which holds the currently active screen. The initial value is Screen.List
, which means the application starts with the List screen. When an item in the list is clicked, we change the state to Screen.Details
, supplying the clicked item’s text. When the Back button is clicked, we change the state back to Screen.List
.
This is probably the simplest possible navigation, and it does not depend on any library or framework. It works, great! But there are some issues:
- The back stack is not preserved in Android, e.g. when the screen is rotated or the application is recreated due to memory constraints. The application will always reset to the List screen.
- UI state is not preserved, the scroll position of the List screen is reset when navigating back.
- There is no animation when switching the screens.
A comprehensive thirty-line navigation
Now let’s see how we can use the Decompose library to easily write custom navigation, so all issues will be resolved.
First of all let’s add the main Decompose dependency to the project:
implementation "com.arkivanov.decompose:decompose:<version>"
Then we need to add one of the extension modules.
For Jetpack Compose:
implementation "com.arkivanov.decompose:extensions-compose-jetpack:<version>"
For Desktop Compose:
implementation "com.arkivanov.decompose:extensions-compose-jetbrains:<version>"
Because we want the back stack to be preserved in Android, we need to make it Parcelable. Decompose provides this interface in Kotlin common code, using Kotlin expect/actual feature. We can use the Parcelize plugin to automatically generate Parcelable implementation for Android. It is also compatible with Kotlin Multiplatform, and so can be used with Desktop Compose.
Now it’s time to write our Composable navigation API. Decompose provides Child Stack, which can be used for navigation. But before we could create Child Stack, we need to supply a dependency.
ComponentContext
ComponentContext is one of the main features of Decompose. Basically, it is an interface that exposes things for lifecycle management, state preservation, instance retaining (aka Android ViewModel) and back button handling. The root ComponentContext must be supplied from an integration point, like Android Activity or Fragment, a main
function, etc. After that Decompose automatically supplies child contexts, thus enabling nested children. You can read more about it in the docs.
We can use Jetpack Compose locals for automatic propagation and nesting of ComponentContext.
Jetpack Compose stack API
Now it’s time to create our stack API.
As you can see, just a few lines of code give us a full featured (and Jetpack Compose friendly) navigation API.
In Decompose navigation is done using child configurations. Configuration is like a child’s descriptor. When we need to add a child to Child Stack, we need to supply its configuration. Child Stack will call the factory function with the provided configuration and child ComponentContext, so we can create the child using arguments from the configuration. Configurations are Parcelable, they can be persisted in Android. When needed, configurations can be restored and children can be recreated with the original arguments.
Our ChildStack
function accepts the following arguments:
source
— a source of navigation commands, used to actually perform the navigation.initialStack
— a stack of initial configurations (from bottom to top) that should be displayed at the beginning.handleBackButton
— controls whether Child Stack should automatically pop the stack on back button press.animation
— allows screen transitions.content
— a Composable lambda displaying a child by the provided configuration.
Usage example
Now let’s use the Child Stack function in our Main component!
We used our Child Stack
function and supplied all the required arguments. The only missing piece now is the root ComponentContext.
Android integration
Here is an example of how we can integrate our Child Stack API in Android.
We created the root ComponentContext using the defaultComponentContext
extension from Decompose. Next we provided the root ComponentContext as local using ProvideComponentContext
function that we created earlier. And lastly we just displayed MainContent
.
Desktop integration
Here is an example of how we can integrate our Child Stack API in Compose for Desktop.
It looks similar to Android integration. However, we have to create the root ComponentContext manually and supply the lifecycle. The lifecycle is controlled by LifecycleController
function, that automatically triggers lifecycle events when necessary. The rest of the code is pretty similar to Android.
Transition animations
In the example above we supplied animation = stackAnimation(fade() + scale())
that provides the following animation.

There are other predefined animations, as well as extensible animation API. More information in the docs.
Conclusion
In this article we created a custom Composable navigation API using Decompose library. The implementation is concise and has some good features like back stack and UI state preservation, back button handling, animations, etc. It is also possible to specify initial back stack, useful for e.g. navigation via deep links.
The proposed API is just an example, there is definitely a room for experiments. Somebody may prefer API more like in Jetpack Compose Navigation. Or create something new.
The complete code and a sample project are available on GitHub.
Thank you for reading the article, and don’t forget to follow me on Twitter!