# Compose Multiplatform Navigation Solutions - JetpackCompose Navigation

In the upcoming posts, I will thoroughly explore several popular navigation libraries that are essential for effective app development. These libraries play a crucial role in managing app navigation, ensuring a smooth and intuitive user experience. Navigation is a key aspect of any app, as it helps users move seamlessly between different screens and features.

In this project, I will showcase JetpackCompose as the primary tool for app navigation. JetpackCompose is a modern toolkit for building native UI, and it offers a powerful and flexible way to handle navigation within your app. By using JetpackCompose, developers can create a more cohesive and responsive user interface.

Since the navigation is being transitioned from Android to a multiplatform project, it is important to explore how JetpackCompose can be utilized in this new context. This transition opens up new possibilities for code sharing and consistency across different platforms. We definitely should give it a try to see how it performs and what benefits it brings to the table. For more information on this topic, you can check out this official guide on [Compose Navigation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html).

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.
    

> The project is available in the [GitHub](https://github.com/mkonkel/JetpackComposeNavigation) repository.

---

# Dependencies

Base project setup, as always, is made with [Kotlin Multiplatform Wizard](https://kmp.jetbrains.com/). We also need to add some navigation-compose dependencies, as it is the core thing that we would like to examine. According to the [documentation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html#sample-project), we should use `navigation` in version `2.8.0-alpha08` and `kotlinx.serialization`.

> Good news are that the new update of compose multiplatform is available. The version `1.7.0-alpha01`that brings the `Safe Args`!

```kotlin
[versions]
compose-plugin = "1.7.0-alpha01"
navigation-compose = "2.8.0-alpha08"
serialization = "1.6.3"

[libraries]
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }

[plugins]
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
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***

```kotlin
plugins {
    alias(libs.plugins.kotlinSerialization)
}

sourceSets {
    commonMain.dependencies {
        ...
        implementation(libs.navigation.compose)
        implementation(libs.serialization.json)
    }
}
```

# Navigation

Getting started with navigation in Compose Multiplatform can seem daunting at first, but it's quite straightforward once you understand the basics. The key question is: how does the navigation system know where to go? The answer lies in the concept of destinations. Each destination has a unique identifier that specifies the current screen to be displayed.

In most cases, a destination will be a composable function. This function represents the UI that will be shown on the screen. By assigning a unique ID to each composable function, the navigation system can easily determine which screen to display based on user actions or other triggers.

> Note:
> 
> In previous versions of navigation the route was defined as a string (you can think of it as the `URL` address). But now we have more robust approach where we can pass an `Object/Class/KClass` as a destination as log as they are serializable.

## Linear Navigation

Let's start with the `Screen` sealed class that will hold the destinations.

```kotlin
@Serializable
sealed class Screen {
    @Serializable
    data object First : Screen()

    @Serializable
    data object Second : Screen()
}
```

Now we can create the `Navigation` composable function, which will hold the `NavHost` and `navigationController`. The `NavHost` acts as the container responsible for displaying the current destination. It ensures that the appropriate composable function is shown based on the navigation state. The `navigationController` is the object that manages the navigation between destinations, allowing us to move from one screen to another seamlessly.

The `NavGraph` is another crucial component in this setup. It maps composable destinations to their respective routes, defining the navigation paths within the app. By setting up the `NavGraph`, we establish the relationships between different screens and specify how users can navigate through them.

```kotlin
@Composable
fun Navigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Screen.First,
    ) {
        composable<Screen.First> {
            FirstScreen(navController)
        }

        composable<Screen.Second> {
            SecondScreen(navController)
        }
    }
}
```

With the navigation framework built, we should implement some screens. The `First` screen will be really simple, with a single button that will navigate to the `Second` screen. The `Second` screen will also be simple, with a button that will navigate back.

```kotlin
@Composable
fun FirstScreen() {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("First screen")
        Button(onClick = { /*TODO navigate to the second screen*/ }) {
            Text("Second Screen")
        }
    }
}
```

```kotlin
@Composable
fun SecondScreen() {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("Second screen")
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { /*TODO navigate back to first screen*/ }) {
            Text("Go Back")
        }
    }
}
```

Now, let's fill in the gaps in the `Navigation()` function with the screens we created. We left some TODOs in the screens that need to be addressed. To navigate between screens, we need to pass the `navController` as an input to our composable functions. For navigation, we will use the `navigate()` method to move to the next screen and the `popBackStack()`method to return to the previous screen.

```kotlin
Button(onClick = { navController.navigate(Screen.Second) }) { Text("Second Screen") }
```

```kotlin
Button(onClick = { navController.navigate(Screen.Second) }) { Text("Second Screen") }
```

With the initial setup complete, the next step is to integrate the `Navigation()` function into the main entry point of the application. For Android, this entry point is typically the `MainActivity.kt` file, while for iOS, it is the `MainViewController.kt` file. In these files, you will initialize the navigation controller and set up the navigation graph to manage the different screens of your app.

```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Navigation()
        }
    }
}
```

```kotlin
fun MainViewController() = ComposeUIViewController { Navigation() }
```

After running the application we should see the `First` scree with a button that navigates to the `Second` screen and a button that navigates back to the `First` screen.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721995990192/b207b5fd-93eb-436c-8fc1-0551e14b0f78.gif align="center")

## Passing parameters

> Note:
> 
> Now with Safe Args passing values is easy, but with the previous release passing arguments was tricky. Since the route looks like the URL address required arguments should be passed as a `path` in route and the optional as `query`

With the `Safe Args` we can pass parameters as the part of the destination object which is easy and convenient, there are two types of arguments **required** and **optional.**

### Required Arguments

```kotlin
@Serializable
sealed class Screen {
    ...
    @Serializable
    data class Third(val greeting: String) : Screen()
}
```

Now we need to create the `ThirdScreen` composable function that will accept the `greetings` parameter and provide a way to pass the arguments. Since the `composable<T>()` is a typed function where `T` is the route from a `KClass` for the destination, we can use the `.toRoute<T>()` function. This extension function returns the route as an object of type `T`. From now on, we can extract the arguments from the passed class. As we know what type of class it is, we also know what type the arguments are.

Since we know that the passing argument is a `String`, we can extract it from the route and pass it to the `ThirdScreen` composable.

```kotlin
@Composable
fun Navigation() {
    NavHost(...) {
        ...
        composable<Screen.Third> {
            val args = it.toRoute<Screen.Third>()

            ThirdScreen(
                navController = navController,
                greetings = args.greeting
            )
        }
    }
}
```

Let's modify the `First` screen to navigate to the `Third` screen with the greeting parameter as an argument of the data class.

```kotlin
@Composable
fun FirstScreen(navController: NavHostController) {
    ...
    Button(
        onClick = {
            val greetings = "Hello from First Screen"
            navController.navigate(Screen.Third(greetings))
        }
    ) {
        Text("Third Screen")
    }
}
```

The `Third` screen should be built in the same way as the `Second` screen, but with the appropriate parameters passed.

```kotlin
@Composable
fun ThirdScreen(navController: NavHostController, greetings: String) {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("Third screen")
        Spacer(modifier = Modifier.height(16.dp))
        Text("Greetings: $greetings")
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { navController.popBackStack() }) {
            Text("Go Back")
        }
    }
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721996603228/afa69c26-ab9b-4834-9d54-ec114e56763d.gif align="center")

### Optional Arguments

For optional arguments we will follow same idea as with required arguments. Let's create the `Fourth` screen that will have two optional arguments `name` and `surname`.

<details data-node-type="hn-details-summary"><summary>Without the safe args</summary><div data-type="detailsContent">With the <code>Safe Args</code>, it's easy to pass optional arguments. In previous versions, we needed to use <code>query</code> parameters where arguments should be passed in the <code>route</code> and preceded by a <code>?</code> character following the pattern <code>?key=value</code>, and if you want to pass multiple optional parameters, they have to be separated with the <code>&amp;</code> character <code>?key1=value1&amp;key2=value2</code>. Also, the optional parameters have to be provided with a default value.</div></details>

With the `Safe Args`, it's easy to pass optional arguments. In previous versions, we needed to use `query` parameters where arguments should be passed in the `route` and preceded by a `?` character following the pattern `?key=value`, and if you want to pass multiple optional parameters, they have to be separated with the `&` character `?key1=value1&key2=value2`. Also, the optional parameters have to be provided with a default value.

```kotlin
@Serializable
data class Fourth(val name: String, val surname: String? = null) : Screen()
```

Now we need to create the `Fourth` screen composable function that will accept the `name` and `surname` parameters.

```kotlin
composable<Screen.Fourth> {
    val args = it.toRoute<Screen.Fourth>()

    FourthScreen(
        navController = navController,
        name = args.name,
        surname = args.surname
    )
}
```

```kotlin
@Composable
fun FourthScreen(navController: NavHostController, name: String, surname: String?) {
    ...
}
```

Navigation is as simple as it can possibly be:

```kotlin
fun FirstScreen(navController: NavHostController) {
    Button(onClick = { navController.navigate(Screen.Fourth(name = "John", surname = "Doe")) }) {
        Text("John Doe Screen")
    }

    Button(onClick = { navController.navigate(Screen.Fourth(name = "Michael")) }) {
        Text("Michael Screen")
    }
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721996846781/e0d0171e-3736-4e36-b1b6-e5afbc6933a1.gif align="center")

## Nested Navigation

In the case of complex applications, splitting navigation into smaller parts is a good idea. Currently, we have one `NavHost` with all screens originating from the same place. We can divide the navigation into smaller parts that will be encapsulated according to their purpose. Let's create a `Fifth` and `Sixth` screen that will be separate from the main navigation and will be accessible only from the `Third` screen. The graph for such screens will look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721997124488/dff8ebe2-f1c1-46ee-98ef-40d69823bb89.png align="center")

With such structured navigation, we can easily manage the navigation and the screens. We can create a `NestedNavigation` composable function that will hold the `NavHost` and `navigationController` for the nested navigation. When we close the `Third` screen, navigation will remove all its children from the backstack, and they won't be accessible anymore. It's a great tool for structuring processes in the application—when a process is finished (for example, a signup, a payment, or a tutorial).

> If you read my post about navigation [Decompose](https://hashnode.com/post/clypbla0r000909l4gov25akn) you can, you will see similarities in the approach. In Decompose, every component can have its own stack and manage it.

Adding the nested navigation graph is accomplished by using the `navigation()` function within the `NavHost` composable. The `navigation()` function requires two key parameters: `startDestination` and `route`. The `route` serves as a unique identifier for the nested navigation graph, ensuring it is distinct from other navigation graphs within the application. This identifier can be a unique name or an object.

The `startDestination` parameter specifies the initial screen that will be displayed when the nested navigation graph is activated. This is the entry point for the nested navigation, guiding users to the first screen they will interact with in this part of the app.

```kotlin
@Serializable
sealed class Route {
    @Serializable
    data object Root : Route()

    @Serializable
    data object Main : Route()

    @Serializable
    data object Nested : Route()
}

@Composable
fun Navigation() {

    NavHost(
        navController = navController,
        startDestination = Screen.First,
        route = Route.Root::class
    ) {
        ...
        navigation(
            startDestination = Screen.Fifth,
            route = Route.Nested::class
        ) {
            composable<Screen.Fifth> {
                FifthScreen(navController)
            }

            composable<Screen.Sixth> {
                SixthScreen(navController)
            }

            composable<Screen.Seventh> {
                SeventhScreen(navController)
            }
        }
    }
}
```

To clarify the navigation, we can split the `Navigation()` function into separate components. The first will handle the `main` graph, and the second will handle the `nested` graph. To do this, we need to create extension functions for `NavGraphBuilder` that will hold specific screens, resulting in the following graph changes:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721997430825/453fc8c2-b713-4b41-9f09-8b308345aa10.png align="center")

```kotlin
fun NavGraphBuilder.nested(navController: NavHostController) {
    navigation(
        startDestination = Screen.Fifth,
        route = Route.Nested::class
    ) {
        // code for nested navigation
    }
}
```

```kotlin
fun NavGraphBuilder.main(navController: NavHostController) {
    navigation(
        startDestination = Screen.First,
        route = Route.Main::class
    ) {
        // code for main navigation
    }
}
```

```kotlin
@Composable
fun Navigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Route.Main,
        route = Route.Root::class
    ) {
        main(navController)
        nested(navController)
    }
}
```

```kotlin
fun FirstScreen(navController: NavHostController) {
    ...
    Button(onClick = { navController.navigate(Route.Nested) }) {
        Text("Nested")
    }
}
```

With such changes, we can still navigate between graphs. There is no problem calling the `Fourth` screen from the nested graph. Let's try to achieve that by adding a way for the `Sixth` screen to open a `Fourth` screen.

```kotlin
@Composable
fun SixthScreen(navController: NavHostController) {
    ...
    Button(onClick = {
        navController.navigate(Screen.Fourth("John", "Doe")) {
            Text("John Doe Screen")
        }
    }
```

We can now modify the `Fourth` screen and add a button that will navigate back to the `main` graph instead of popping back the stack. This way, we can close the `nested` graph immediately and remove all its child screens from the backstack. The `navigate()` builder has a `popUpTo()` method that allows us to remove destinations from the backstack. We can pass the destination to which we want to pop back. There is also the `inclusive` parameter to remove the passed destination from the backstack as well.

```kotlin
@Composable
fun FourthScreen(...) {
    ...
    Button(
        onClick = {
            navController.navigate(Route.Main) {
                popUpTo(Route.Main)
            }
        }
    ) {
        Text("MAIN")
    }
}
```

We can close the `nested` graph even quicker, while opening the `Fourth` screen from the `Sixth` all we need to do is use `popUpTo()` method with the `Route.Nested` parameter.

```kotlin
@Composable
fun SixthScreen(...) {
    ...
    Button(onClick = {
        navController.navigate(Screen.Fourth("John", "Doe")) {
            popUpTo(Route.Nested)
        }
    }) {
        Text("John Doe Screen")
    }
}
```

You can combine these functions in various ways to achieve the desired behavior. For instance, you can `pop` a screen before navigating to a new one, or you can remove entire navigation graphs from the backstack. This flexibility allows you to manage your app's navigation flow precisely.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721997669507/6a050685-bdbf-4139-8d5e-1f59d3927319.gif align="center")

## Bottom Navigation

Yet another thing that is widely common in mobile apps nowadays is the bottom navigation. Let's extend the project with one more feature! We need to add three new screens: the `Eighth` screen, which will be the main screen that holds the bottom menu and is a container for the tabs, and the `Ninth` screen and `Tenth` screen. Inside the `Eighth` screen, we will add a new `NavHost` that will build its own graph and handle switching tabs. We will also use the `BottomNavigation` Jetpack Compose control to create the bottom bar view and its items.

```kotlin
@Composable
fun EighthScreen() {
    val navController = rememberNavController()

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            // TODO: add bottom navigation
        },
    ) { innerPadding ->
        NavHost(
            modifier = Modifier.padding(innerPadding),
            navController = navController,
            startDestination = Screen.Eighth.Tab.Home,
        ) {
            composable<Screen.Eighth.Tab.Home> {
                NinthScreen()
            }

            composable<Screen.Eighth.Tab.Edit> {
                TenthScreen()
            }
        }
    }
}
```

The new `NavHost` has its own `navController` and `startDestination`. When we enter the screen, the item displayed as the first tab will always be the `Ninth` screen. The local `navController` is used to navigate between tabs. The `BottomNavigation` control is quite helpful. It will render the bottom bar with necessary elements such as **icon**, **label**, and **selected** state, and even adds a slight dim to the selected item. But to do so, we need to provide information about the tabs. Like in every other type of navigation, the displayed screens need their own `route`/`destination`, so we need to create a new `sealed class` for the tabs inside the current `Screen.kt` file.

```kotlin
@Serializable
sealed class Screen {
    ...
    @Serializable
    data object Eighth : Screen() {
        @Serializable
        sealed class Tab(val icon: ICON, val label: String) : Screen() {
            @Serializable
            data object Home : Tab(icon = ICON.HOME, label = "Home")

            @Serializable
            data object Edit : Tab(icon = ICON.EDIT, label = "Edit")

            @Serializable
            enum class ICON {
                HOME, EDIT
            }
        }
    }
}
```

Now we can create the bottom navigation tabs.

```kotlin
@Composable
private fun BottomBar(navController: NavHostController) {
    val tabs = listOf(
        Screen.Eighth.Tab.Home,
        Screen.Eighth.Tab.Edit,
    )

    val backstackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = backstackEntry?.destination

    BottomNavigation {
        tabs.forEach { tab ->
            TabItem(tab, currentDestination, navController)
        }
    }
}
```

The `BottomBar` is a composable function that is responsible for managing the elements within the tabs container. Inside this function, we need to define the elements, referred to as `tabs`, that will be displayed in the bottom bar. These tabs are specified as a list, which includes `Screen.Eighth.Tab.Home` and `Screen.Eighth.Tab.Edit`.

To keep track of the current screen, we utilize the `currentBackStackEntryAsState()` function. This function helps us obtain the current destination by monitoring changes in the `navController`. Whenever the `navController` undergoes changes due to navigation actions like `navigate()` or `pop()`, the `currentBackStackEntryAsState()` function updates the value, triggering a recomposition of the UI. This means that the top entry on the backstack is returned, allowing us to determine what is currently displayed on the screen.

By retrieving the `destination` from the backstack entry, we can access detailed information about the current screen. This setup ensures that the bottom navigation bar is dynamic and responsive to changes in the navigation state.

The `BottomNavigation` control takes a few parameters, and the last one is the `content: @Composable RowScope.() -> Unit`, which will be responsible for creating the bottom navigation view. For each tab that we want to display, we should create a proper UI element. We can create an extension function for `RowScope` that will be responsible for providing the `BottomNavigationItem` for each tab.

```kotlin
@Composable
private fun RowScope.TabItem(
    tab: Screen.Eighth.Tab,
    currentDestination: NavDestination?,
    navController: NavHostController,
) {
    BottomNavigationItem(
        icon = { Icon(imageVector = tab.icon.toVector(), contentDescription = "navigation_icon_${tab.label}") },
        label = { Text(tab.label) },
        selected = currentDestination?.hierarchy?.any { it == tab } == true,
        onClick = {
            navController.navigate(tab) {
                navController.graph.startDestinationRoute?.let { popUpTo(it) }
                launchSingleTop = true
            }
        },
    )
}

// helper function for transforming enum to vectorIcon
private fun Screen.Eighth.Tab.ICON.toVector() = when (this) {
    Screen.Eighth.Tab.ICON.HOME -> Icons.Default.Home
    Screen.Eighth.Tab.ICON.EDIT -> Icons.Default.Edit
}
```

The `selected` state is calculated by checking if the current destination is the same as the tab of the current item. The `onClick` action is responsible for navigating to the clicked tab.

Since we want only one active screen inside the tabs container we need to `pop` it. This will cause dropping other element from the back stack. We can also add the `launchSingleTop` which will ensure that the tab is not preserved, and will be recreated with every click.

Last thing to do is to add an entry point in the main graph.

```kotlin
fun NavGraphBuilder.main(navController: NavHostController) {
    ...
    composable<Screen.Eighth> {
        EighthScreen()
    }
}
```

```kotlin
@Composable
fun FirstScreen(navController: NavHostController) {
    ...
    Button(onClick = { navController.navigate(Screen.Eighth) }) {
        Text("Bottom")
    }
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722226312723/326dc4d1-e890-46fc-900a-34c57e8687be.gif align="center")

# Coroutines

The last topic we will cover in this post is asynchronous operations. As mentioned earlier, various navigation libraries like [Decompose](https://hashnode.com/post/clypbla0r000909l4gov25akn), Appyx, and Voyager offer their own business logic container objects where asynchronous operations can be managed effectively.

In the context of Jetpack Compose, we will be utilizing `ViewModels` to manage these asynchronous tasks. `ViewModels` have recently been integrated into the Compose Multiplatform library, making them a versatile choice. This integration allows us to leverage the full power of `ViewModels` while building cross-platform applications with Compose.

It's important to note that even though Appyx and Voyager come with their own mechanisms for handling business logic and async operations, they still provide the flexibility to use `ViewModels` if you prefer. This means you can choose the approach that best fits your project's architecture and requirements.

The first thing to add is the proper dependency according to the [documentation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-viewmodels.html).

```kotlin
[versions]
common-viewmodels = "2.8.0"

[libraries]
viewmodels-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "common-viewmodels" }
```

```kotlin
commonMain.dependencies {
    ...
    implementation(libs.viewmodels.compose)
}
```

The general usage of the `ViewModel` in Jetpack Compose is straightforward and bears a strong resemblance to the traditional Android approach. To begin with, we need to create a class that extends the `ViewModel`. This class will serve as the container for our business logic and asynchronous operations.

Within this `ViewModel` class, we have access to the `viewModelScope`, which is a specialized `CoroutineScope` tied to the lifecycle of the `ViewModel`. The significance of the `viewModelScope` is that it ensures any coroutines launched within it are automatically canceled when the `ViewModel` is destroyed. This lifecycle-aware behavior is crucial for managing resources efficiently and avoiding memory leaks.

For instance, if we need to perform a network request or a database operation, we can launch these tasks within the `viewModelScope`. This makes it easy to handle asynchronous operations without worrying about manually canceling coroutines when the `ViewModel` is no longer needed. Additionally, this setup allows us to write cleaner and more maintainable code, as the `ViewModel` encapsulates the logic and handles the lifecycle management for us.

Let's create a simple `ViewModel` that will handle the countdown timer and the corresponding `Eleventh` screen for displaying the values.

```kotlin
class EleventhViewModel : ViewModel() {
    private val _countDownText = MutableStateFlow("")
    val countDownText: StateFlow<String> = _countDownText.asStateFlow()

    init {
        viewModelScope.launch {
            for (i in 10 downTo 0) {
                _countDownText.value = i.toString()
                delay(1000)
            }
        }
    }
}
```

To use the `ViewModel`, we can utilize the `viewModel` function provided by the `lifecycle-viewmodel-compose` library. The function returns an existing view model or creates a new one in the scope. The created `ViewModel` is bound to the `viewModelStoreOwner` and will be retained as long as the scope is alive.

The source code of the `Eleventh` screen is quite simple and looks like all previously created screens.

```kotlin
@Composable
fun EleventhScreen(
    navController: NavHostController,
    viewModel: EleventhViewModel = viewModel { EleventhViewModel() },
) {
    Column(...) {
        ..
        Countdown(viewModel)
    }
}

@Composable
private fun Countdown(viewModel: EleventhViewModel) {
    val countdownText = viewModel.countDownText.collectAsState().value
    Text("COUNTDOWN: $countdownText")
}
```

After adding the created screen to the navigation, we can launch it from the `First` screen end examine the countdown functionality.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1722227183303/3bc8166e-4d0b-44e7-aa60-12f916a4cfcd.gif align="center")

# Summary

In this post, we covered the basics of Jetpack Compose navigation. The library is well-documented, and you will surely find many posts, videos, and other resources to help you understand the navigation better. Jetpack Compose navigation is a powerful tool that allows you to create complex navigation structures with ease. The library has been available for Android for a long time, and now it's available for multiplatform projects. The multiplatform version of the library is still in **alpha** version, but in my opinion, it's ready to use in production. The library is a great choice for developers who want to create modern and complex navigation structures in their applications.

In my previous posts, I covered the [Decompose](https://hashnode.com/post/clypbla0r000909l4gov25akn), Appyx, and Voyager navigation libraries. Now I can say that my personal favorite is the `JetpackCompose` navigation, as it will be the most popular and widely used in the future, with great support from the community and the Jetbrains team. The Compose Multiplatform is growing rapidly, and I'm sure that in the near future, we will see a lot of great libraries and tools that will help us create modern and complex applications. But all of them are great, and you should choose what fits your needs best.

When I began writing a post about [Fullstack Kotlin Developer](https://github.com/mkonkel/GameShop), I was thinking about the navigation library that I would use in the project. As there was no official or suggested way of handling the navigation in the multiplatform project, I chose the `Decompose` library. However, after adopting the `JetpackCompose` navigation and the `Viewmodels` to `Compose Multiplatform`, I would definitely recommend it as the best choice for your project, and I'm sure that I will use it in all of my future projects as well.

> In this project, I will explore several popular navigation libraries for app development, emphasizing the use of JetpackCompose for managing navigation in a multiplatform context. I will cover linear navigation, passing parameters safely, nested navigation, bottom navigation, and handling asynchronous operations with coroutines using ViewModels. The content highlights how to set up navigation components, manage navigation state, and integrate bottom navigation into your app, along with a practical guide on using ViewModels for managing asynchronous tasks. This article aims to demonstrate how JetpackCompose is a versatile and modern solution for implementing complex navigation structures in your applications.
