DecomposeNavigation
Showcase of the Decompose navigation in Compose Multiplatform app
Install / Use
/learn @mkonkel/DecomposeNavigationREADME
Decompose Navigation
This is a Kotlin Multiplatform project targeting Android and iOS where I will showcase the Decompose as the app navigation.
Assumptions:
- Application should allow us to navigate from one screen to another.
- Application should allow to pass some parameters from first to second screen.
- Application should handle the screen rotation without loosing data.
- Application should handle the Tab Navigation.
- Application should handle the async operations with coroutines.
In the next posts I will also cover the Voyager, Apyx and JetpackCompose navigation libraries.
The project:
Base project setup as always is made with Kotlin Multiplatform Wizard, we also need to add some Decompose as it is the core thing that we would like to examine. There is also one thing that we need to add to the project and that is the Kotlin Serialization plugin.
libs.versions.toml
[versions]
decompose = "3.0.0-beta01"
serialization = "1.6.3"
[libraries]
decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
decompose-compose = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
[plugins]
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Freshly added dependencies needs to be synced with the project and added to the build.gradle.kts
plugins {
alias(libs.plugins.kotlinSerialization)
}
sourceSets {
androidMain.dependencies {
...
implementation(libs.decompose)
}
commonMain.dependencies {
...
implementation(libs.decompose)
implementation(libs.decompose.compose)
implementation(libs.serialization)
}
}
Now we can sync the project and start coding. Following the Decompose documentation we can notice that the main element of the library is the Component class that is encapsulating logic (and other components). Components are lifecycle-aware with their own lifecyclethat is automatically managed. its lifecycle is very similar to the androids activity lifecycle. Components are independent of the UI and the UI should relay on the components. The idea is to hold as much code in the shared logic as possible - components are responsive for holding business logic and the navigation itself (the navigation is separated from teh UI). If you are familiar with the Android development you can think of the components as the ViewModel.
Each component should have a ComponentContext that manages its lifecycle, keeps it state (can preserve component state during changes) and handles back button. The context is passed through the constructor and can be added to the component by the delegation.
As mentioned above the main point of the app should be a RootComponent which should be provided with the ComponentContext to determine how it should act on different platforms. Therefore, it's context cannot be provided and must be created on the platform itself. For such situations, we can use the DefaultComponentContext() - if it's created inside the @Composable function we should always use the remember{} so the context will not be created with every recomposition.
With that covered we can start to code, lets create a navigation package in our project with the RootComponent. The RootComponent will live as long as the application.

class RootComponent(
componentContext: ComponentContext
) : ComponentContext by componentContext {
// Some code here
}
Let's assume that our application will hold two screens - FirstScreen and SecondScreen. Both of them will be represented by the Component class. The FirstScreen will be the first screen that will be shown to the user and the SecondScreen will be shown after the user clicks the button on the FirstScreen. To handle such case we need to create a Stack in the RootComponent - the stack is provided to the component via the ComponentContext. Every stack requires the Configuration that needs to be @Serializable, it will represent the child components and contains all arguments needed to create it.
@Serializable
sealed class Configuration {
@Serializable
data object FirstScreen : Configuration()
@Serializable
data class SecondScreen(val text: String) : Configuration()
}
The created configuration can be used now in the stack creation. We should use the StackNavigator interface. It contains the methods needed to handle the process, such as navigate(), push(), pop() etc...
private val navigation = StackNavigation<Configuration>()
The definitions of child components are created by the Configuration, but now e need also to create Child Components itself. Components are organised as trees, where the root component is the main component and the child components are the components that are created by the main component. The parent component knows only about it's direct children. Every component ca be independently reuse in every place in the app. With the usage of the navigation components are automatically created and destroyed, and they need a provided component context from the parent. Let's now focus on the Child Stack approach, but you can find other solutions in the docs.
During the navigation, the child stack compares new configurations with previous one, there should be only one (the top) component active, others are in the back and stopped or destroyed.
class FirstScreenComponent(
componentContext: ComponentContext
) : ComponentContext by componentContext {
// Some code here
}
class SecondScreenComponent(
componentContext: ComponentContext,
private val text: String
) : ComponentContext by componentContext {
// Some code here
}
With new components added we now need to create them inside the root component - they should be called children.
sealed class Child {
data class FirstScreen(val component: FirstScreenComponent) : Child()
data class SecondScreen(val component: SecondScreenComponent) : Child()
}
The last thing to do is to create the childStack. The childStack requires some parameters to be passed, such as the source of the navigation, the serializer, the initial configuration, the handleBackButton and the childFactory. The childFactory is a function that creates the child component based on the configuration and component context. The childStack is responsible for creating the child components and managing their lifecycle.
val childStack = childStack(
source = navigation,
serializer = Configuration.serializer(),
initialConfiguration = Configuration.FirstScreen,
handleBackButton = true,
childFactory = ::createChild
)
private fun createChild(configuration: Configuration, componentContext: ComponentContext): Child =
when (configuration) {
is Configuration.FirstScreen -> Child.FirstScreen(FirstScreenComponent(componentContext))
is Configuration.SecondScreen -> Child.SecondScreen(SecondScreenComponent(componentContext, configuration.text))
}
ChildStack cannot be empty, it has to have at leas active (resumed) child component. Component in the back are always stopped. If we want to use multiple ChildStacks in one component all of them has to have unique key associated. If we examine the childStack we can notice that it is a Value type.

The Value is a type that represents a value that can be observed is the Decomposes equivalent of Jetpack Compose State, it is also independent of the approach u want to use further in the application. Nevertheless, in the * Compose Multiplatform* approach it can (and should) be transformed to the state.
With all things done, we can now handle the actual navigation, following
the documentation
we can handle it with multiple ways - with traditional callbacks or with a bit more reactive approach with flow
or observable. It's all upon to yuo how you want to communicate child components with the root component.
You can also create a global navigation object that will be responsible for changing the screens from any place in
the app, there is no good or bad practice. For the simplification of the example, I will use the callbacks.
In the firstScreen I will add a lambda expression on onButtonClick: (String) -> Unit that will be called when
the button is clicked. The lambda will be called with the greetings text, and handled in the RootComponent.
class FirstScreenComponent(
componentContext: ComponentContext,
private val onButtonClick: (String) -> Unit,
) : ComponentContext by componentContext {
fun click() {
onButtonClick("Hello from FirstScreenComponent!")
}
}
Now I need to impleme
Related Skills
node-connect
347.6kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
108.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
347.6kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
347.6kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
