
Decompose — experiments with Kotlin Multiplatform lifecycle-aware components and navigation
Kotlin Multiplatform is a technology that enables code sharing between different platforms. It is completely up to developers how much of the code they want to share. But there are cases for which we just can’t write shared code, even if we would like to.
One of the cases is UI. Currently it is not possible to make UI code common. And actually this particular case is totally fine — Kotlin Multiplatform’s intention is to share business logic, not UI.
Another case is navigation. We can implement all the business logic in common and add platform specific UI. But navigation between screens has to be also implemented separately in each platform. E.g. in Android we can use Badoo/RIBs framework or AndroidX Fragments. And in iOS we can similarly use ViewControllers.
But would not it be great to share navigation logic as well? So the only platform specific code we would have is UI (plus expect/actual).
Decompose
I have been experimenting with shared navigation for the last month, and this project is the result. Decompose is inspired by Badoo RIBs fork of the Uber RIBs framework. It is intended to address the following points:
- Multiplatform: Android, iOS and JavaScript
- Encapsulation of business logic into lifecycle-aware components (aka BLoCs)
- Nested components
- Routing (or navigation) between components
- Components’ state preservation (including back stack)
- Instances retaining in components (aka ViewModels in Android)
- Pluggable platform-specific UI (primarily declarative UI frameworks, such as Jetpack Compose, SwiftUI, JavaScript React, etc.)
Let’s take a closer look at what the Decompose library offers.
Pluggable UI
Normally components (such as Android Fragments or iOS ViewControllers) encapsulate UI. But in Kotlin Multiplatform it is very important to allow pluggable platform specific UI. This is quite easy to achieve when using declarative UI frameworks, such as Jetpack Compose, SwiftUI or JavaScript React. Decompose is designed with this functionality in mind. Using normal Android Views is also possible, though.
The simple idea is to expose components’ state and to observe the state in the UI. Decompose provides Value
and MutableValue
interfaces and their builders and extensions for exposing state from components.
This is what a simple Counter component might look like. All it does is increase its value by one on an external event.
So in the simplest case, a component is just a class. That’s enough to be a component!
Here is an example of Android UI using Jetpack Compose:
And iOS UI using SwiftUI:
Lifecycle-aware Components
In Decompose each component has a Lifecycle. The Lifecycle is very similar to what we have in Android. This is how its interface looks like:
The Lifecycle can be in one of the predefined states and can be listened for changes. Here is the diagram showing possible transitions between states:

The Lifecycle changes its state under various circumstances, such as the component is pushed to or popped from the back stack, or the application is minimized, etc. This works very similar to the AndroidX Fragment lifecycle, so you can read more about it in the documentation.
ComponentContext
In Decompose every component has a ComponentContext. It is nothing more than an interface that gives components access to some features, like the Lifecycle.
When we create a root component we have to create ComponentContext manually. There are various helper functions and default implementations to simplify this process. Child contexts are provided by the Router for every child component. When required, it can be passed normally via component’s constructor.
Nested components
Each component can encapsulate children components, so they can be represented as a tree. The Lifecycle of children can not be longer than that of their parents. So if a parent is destroyed then all its children are destroyed as well.

Routing
This is the most interesting part. In Decompose routing functionality is provided by the Router. Here is the Router interface:
In general, the Router is just a stack of components. Currently only two operations are supported: push and pop. Each time a new component is pushed into Router, it is created and resumed, and the currently active component is stopped (but not destroyed) and is moved to the back stack. And when the currently active component is popped from the Router, it is destroyed and the most recent component in the back stack is resumed again. Each component can have multiple Routers.
This makes it possible to implement shared navigation logic.

State and back stack preservation
This is very important especially in Android. When configuration changes (e.g. screen orientation) or process is recreated, all active components are restored in their previous state. Moreover, the back stack is also restored. This is possible because each component in the back stack has an associated persistent configuration. Configurations are defined by parents and are used for children instantiation.
Here is an example of routing:
Each Router has at least two arguments:
- Initial configuration — a configuration of the very first component, that will be created and pushed to the Router if there is no saved state yet
- Component factory — a function that creates components by configuration
Since all configurations are persistent, they are automatically saved and restored by the Router, and are passed to the component factory for recreation.
Decompose defines the Parcelable
and @Parcelize
interfaces using expect/actual declarations, so we can use them in common code. Parcelable implementations generator (aka Parcelize) works just fine with Kotlin Multiplatform. Saving state in other platforms is to be investigated.
Custom state preservation is also possible via StateKeeper API:
Instances retaining
In Android view models are retained (not destroyed) when configuration changes. This is used to keep existing data in memory and to avoid interrupting background tasks. Decompose provides similar InstanceKeeper API:
Samples
There are two sample projects available in the repository:
- Counter — for Android, iOS and JavaScript
- Master-Detail — for Android and Desktop, more to come
There is also the Todo List example app available in the compose-jb repository.
All together they cover many use cases, such as:
- Nested components
- Routing (including nested navigation)
- State preservation and instances retaining
- Pluggable UI (Jetpack Compose, SwiftUI, Kotlin React)
- Multi-module structure
- Inter-component communication
- Single-pane/multi-pane layouts
Library status
Decompose is version 0.0.7 at the time of this posting and is more like an experiment, let’s see how it goes. However, it is published and ready to try, might be a good fit for a pet project. Any feedback is appreciated.
Thanks for reading the article, and don’t forget to follow me on Twitter!