# Compose Multiplatform Navigation Solutions - Appyx

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. In today's episode, we will focus on the [Appyx library](https://github.com/bumble-tech/appyx). We will examine how Appyx handles various navigation scenarios, its integration process.

Choosing a navigation library for your Compose Multiplatform project can be challenging. The decision requires careful consideration of your project's requirements and future scalability, understanding the strengths and limitations of each library is crucial to avoid potential pitfalls and ensure a seamless development process.

Showcase application 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/AppyxNavigation) *repository.*

---

# Dependencies

Base project setup, as always, is made with [Kotlin Multiplatform Wizard](https://kmp.jetbrains.com/). We also need to add [Appyx](https://kmp.jetbrains.com/) as it is the core element we want to examine. Appyx consists of three main libraries that complement each other: ***navigation***, ***interactions***, and ***components***. This allows us to create a fully customized application.

`libs.versions.toml`

```kotlin
[versions]
appyx = "2.0.1"

[libraries]
appyx-navigation = { module = "com.bumble.appyx:appyx-navigation", version.ref = "appyx" }
appyx-interactions = { module = "com.bumble.appyx:appyx-interactions", version.ref = "appyx" }
appyx-components-backstack = { module = "com.bumble.appyx:backstack", version.ref = "appyx" }
```

Freshly added dependencies need to be synced with the project and added to the `build.gradle.kts`.

```kotlin
sourceSets {
    commonMain.dependencies {
        ...
        implementation(libs.appyx.navigation)
        implementation(libs.appyx.interactions)
        api(libs.appyx.components.backstack)
    }
}
```

# Appyx Introduction

With dependencies added, we can start to create the navigation. Following the Appyx [documentation](https://bumble-tech.github.io/appyx/navigation/), we can notice one major thing: Appyx gives us flexibility in interpreting the term "navigation." Most modern libraries/solutions focus on how to get from one screen to another, but Appyx gives us the possibility to create navigation that is not only about screens but "viewport." It can be whatever you can imagine, for example, spinning the carousel.

## Node

Nevertheless, we will focus on traditional `Stack` navigation. There are some basic blocks that we need to use. The first one is `Node`, which is a representation of the structure (in our case, the screen). Each node can hold other nodes, and they are called ***children***. The `node` is a standalone unit with its own:

* [AppyxComponent](https://bumble-tech.github.io/appyx/components/) - in our case, it will be a `back stack`, with simple linear navigation. The element at the front is considered active, others are stashed. It can never be empty. It has some basic helper functions like `push`, `pop`, `replace`, and a default back handler.
    
* [Lifecycle](https://bumble-tech.github.io/appyx/navigation/features/lifecycle/) - it's a multiplatform interface that notifies the component about the state changes on the platform. For example, on the Android platform, it is implemented with `AndroidLifecycle`.
    
* State Restoration after orientation changes
    
* View, which is created with `@Composable`
    
* Business logic
    
* Plugins - since `Nodes` should be kept lean, the plugins are used to add additional functionality, for example, `NodeLifecycleAware` that allows listening to the lifecycle events.
    

The `nodes` can be as small as you want to keep the complexity of your logic low and encapsulated, and extracted to the **children** to compose the navigation process. With the nodes, your navigation can work as a tree with multiple branches responsible for different processes. Some parts of the tree are active - visible on the screen, while others are stashed. To change what's currently active, we will use the component; the change will look like navigation by adding or removing nodes.

Such an approach creates a [Scoped DI](https://bumble-tech.github.io/appyx/navigation/features/scoped-di/), a situation where if the parent node is destroyed, all of its children nodes and related objects are released. There is also [ChildAwareAPI](https://bumble-tech.github.io/appyx/navigation/features/childaware/) that helps with communication between the parent and dynamically added child.

## RootNode

After that short introduction it's time to code. First thing that we need to create is ***RootNode***.

```kotlin
class RootNode(
    nodeContext: NodeContext,
) : LeafNode(nodeContext) {

    @Composable
    override fun Content(modifier: Modifier) {
        Column(
            modifier = Modifier.fillMaxSize().then(modifier),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Hello Appyx!")
        }
    }
}
```

Similarly, as in the [Decompose](https://hashnode.com/post/clypbla0r000909l4gov25akn) library, we need to provide context. The `NodeContext` is created on the host platform (e.g., Android's MainActivity) and is passed down to all the descendants. It ensures the support of lifecycle and keeps the structured hierarchy of children nodes. The `LeafNode` uses the context to handle all the lifecycle events, manage plugins, provide the coroutines scope, and manage the children creation to keep the structured nodes hierarchy mentioned in ***scoped DI***. It also forces us to implement a ***@Composable*** function `Content` that will be used to create the view.

### Android

Let's connect the `RootNode` with the hosts. For Android, we need to use ***MainActivity*** and inherit from the `NodeComponentActivity()`, which integrates Android with Appyx under the hood (if you don't want to inherit from ready-made solutions, you can implement ActivityIntegrationPoint yourself). Then we need to create the `NodeHost` (which is responsible for providing ***nodeContext***) and provide it with ***lifecycle***.

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

        setContent {
            MaterialTheme {
                NodeHost(
                    lifecycle = AndroidLifecycle(LocalLifecycleOwner.current.lifecycle),
                    integrationPoint = appyxIntegrationPoint
                ) { nodeContext ->
                    RootNode(nodeContext)
                }
            }
        }
    }
}
```

### iOS

With the iOS we just need to create proper host the `IosNodeHost` with default `IntegrationPoint` as it is a `@Compose` function.

```kotlin
fun MainViewController() = ComposeUIViewController {
    IosNodeHost(
        modifier = Modifier,
        integrationPoint = MainIntegrationPoint(),
        onBackPressedEvents = backEvents.receiveAsFlow()
    ) { nodeContext ->
        RootNode(nodeContext)
    }
}
```

# Linear Navigation

The basic setup was done, we were able to display initial screen with the text. Now we can add children screens. The screen definition need to be defined as `Parcelable` it will tell the node where we ant to go. The `Parcelable`is a part of Appyx library and it's expect/actual class so it has a common definition that is implemented differently on platforms. For android platform we need to add the support od [kotlin-parcelizeplugin to use `@`](https://developer.android.com/kotlin/parcelize)`Parcelize` annotation.

```kotlin
plugins {
    ...
    id("kotlin-parcelize")
}
```

Now we can define some navigation targets to enhance our app's functionality. To do this, we need to specify the screens we want to navigate to. Each screen should be defined as a `Parcelable` object, which will inform the node about the destination.

```kotlin
sealed class NavTarget : Parcelable {
    @Parcelize
    data object FirstScreen : NavTarget()

    @Parcelize
    data object SecondScreen : NavTarget()
}
```

Targets are defined, so now the `RootNode` needs to be modified to handle the `BackStack`component and provide a way to navigate between different screens. The first change we need to make is to transform the `RootNode` from being merely a `LeafNode` to a `Node<>`. A `LeafNode` is a simple node that cannot have children and is primarily used to display static content. On the other hand, a `Node<>` is more versatile as it can have child nodes and is capable of managing navigation between them.

To achieve this, we need to update the `RootNode` class to include the `BackStack`component, which will keep track of the navigation history. This allows the app to handle back navigation correctly and ensures that users can move seamlessly between different screens.

The `Node` requires the `NavTarget` to be defined and the `buildChildNode` function to be implemented. These two elements are crucial because they handle the creation and management of the child nodes. The `NavTarget` serves as a blueprint for the different screens or destinations within the app, while the `buildChildNode` function is responsible for constructing these child nodes whenever navigation occurs.

Additionally, another important aspect to consider is the `appyxComponent` parameter. This parameter plays a significant role in defining how the nodes will be managed and navigated. It determines the behavior and lifecycle of the nodes, ensuring that they are correctly instantiated, displayed, and disposed of as the user navigates through the app.

```kotlin
private fun backstack(nodeContext: NodeContext): BackStack<NavTarget> = BackStack(
    model = BackStackModel(
        initialTarget = NavTarget.FirstScreen,
        savedStateMap = nodeContext.savedStateMap,
    ),
    visualisation = { BackStackFader(it) }
)
```

The destination nodes may look like the following:

```kotlin
class FirstNode(
    nodeContext: NodeContext
) : LeafNode(nodeContext = nodeContext) {
    @Composable
    override fun Content(modifier: Modifier) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Hello from the First Node!")
        }
    }
}
```

To enable navigation between different screens, we will add simple lambda expressions. These lambda expressions will be invoked within the child nodes, but the actual handling will occur in the root node. This approach allows us to centralize the navigation logic, making it easier to manage and maintain.

```kotlin
class FirstNode(
    private val onButtonClick: () -> Unit
) : LeafNode(nodeContext = nodeContext) {
    @Composable
    override fun Content(modifier: Modifier) {
        ...
        TextButton(onClick = onButtonClick) {
            Text("Go to Second Node")
        }
    }
}
```

```kotlin
class RootNode(
    nodeContext: NodeContext,
    private val backstack: BackStack<NavTarget> = backstack(nodeContext),
) : Node<NavTarget>(
    appyxComponent = backstack,
    nodeContext = nodeContext,
) {
    override fun buildChildNode(navTarget: NavTarget, nodeContext: NodeContext): Node<*> =
        when (navTarget) {
            NavTarget.FirstScreen -> FirstNode(nodeContext) {
                backstack.push(NavTarget.SecondScreen)
            }
            NavTarget.SecondScreen -> SecondNode(nodeContext) {
                backstack.push(NavTarget.FirstScreen)
            }
        }
}
```

The last step involves adding the [**AppyxNavigationContainer**](https://bumble-tech.github.io/appyx/interactions/usage/?h=appyxnavigationcontainer#in-the-scope-of-appyx-navigation). This container is crucial as it will manage the navigation and render the content of the nodes that we have added.

```kotlin
@Composable
override fun Content(modifier: Modifier) {
    AppyxNavigationContainer(appyxComponent = backstack)
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721627965690/1d3f3353-1f23-4d6b-802b-698c6b1c304b.gif align="center")

You can experiment with different visualisations, for example `BackStackFader`, `BackStackSlider`, `BackStackParallax`or other mentioned in the [documentation](https://bumble-tech.github.io/appyx/components/backstack/)

# Tab Navigation

To handle the bottom navigation feature, we need to use a [Spotlightcomponent](https://bumble-tech.github.io/appyx/components/spotlight/). This component functions similarly to an Android view pager, allowing us to manage multiple nodes simultaneously while keeping one of them active and visible at any given time. The Spotlight component is designed to facilitate smooth transitions and intuitive navigation within the app.

The principle is the same as with linear navigation, where we use a `backstack` to manage the navigation flow. However, for bottom navigation, we switch from using `backstack` to `spotlight`. This switch enables us to handle multiple nodes more efficiently and provides a better user experience by maintaining the state of each node while navigating between them.

## Spotlight

To implement this, we first need to define the nodes that will be part of the bottom navigation. Each node represents a different screen or feature within the app. Once the nodes are defined, we can use the Spotlight component to manage their visibility and transitions. Additionally, the Spotlight component offers various customization options, such as animations and transition effects, to enhance the user experience.

```kotlin
appyx-components-spotlingh = { module = "com.bumble.appyx:spotlight", version.ref = "appyx" }
```

Let's create new navigation targets for tabbed navigation.

```kotlin
sealed class SpotlightNavTarget : Parcelable {
    @Parcelize
    data object ThirdScreen : SpotlightNavTarget()

    @Parcelize
    data object FourthScreen : SpotlightNavTarget()
}
```

Now we need to create a parent node that will hold the spotlight component.

```kotlin
class SpotlightNode(
    nodeContext: NodeContext,
    private val model: SpotlightModel<SpotlightNavTarget> = spotlightModel(nodeContext),
    private val spotlight: Spotlight<SpotlightNavTarget> = Spotlight(
        model = model,
        visualisation = { SpotlightSlider(it, model.currentState) }
    ),
) : Node<SpotlightNavTarget>(
    appyxComponent = spotlight,
    nodeContext = nodeContext,
)
```

We have to provide `SpotlightModel` and `SpotlightVisualisation`. One will handle navigation, and the other will handle the animation. The model takes a list of elements available to be displayed in the carousel and the initial index of the default active tab.

```kotlin
private fun spotlightModel(nodeContext: NodeContext) = SpotlightModel(
    items = listOf(SpotlightNavTarget.ThirdScreen, SpotlightNavTarget.FourthScreen),
    initialActiveIndex = 0f,
    savedStateMap = nodeContext.savedStateMap
)
```

Last thing to do is to create a UI representation of the screen, we will use Scaffold and default button

```kotlin
@Composable
override fun Content(modifier: Modifier) {
    Scaffold(
        bottomBar = {
            Row(Modifier.background(Color.White)) {
                TextButton(modifier = Modifier.weight(1f), onClick = { spotlight.first() }) {
                    Text(text = "Third")
                }
                TextButton(modifier = Modifier.weight(1f), onClick = { spotlight.last() }) {
                    Text(text = "Fourth")
                }
            }
        }
    ) { paddings ->
        AppyxNavigationContainer(modifier = Modifier.padding(paddings), appyxComponent = spotlight)
    }
}
```

The targets nodes will be exactly the same but with a different text and a background.

```kotlin
class ThirdNode(
    nodeContext: NodeContext,
) : LeafNode(nodeContext = nodeContext) {
    @Composable
    override fun Content(modifier: Modifier) {
        Column(
            modifier = Modifier.fillMaxSize().background(Color.Magenta),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Hello from the Third Node!")
        }
    }
}
```

The solution is ready to use, as it was designed to work just out of the box. We need to add only the entry point to our existing navigation. We need to add `TabScreen` as a `NavTarget` in the linear navigation and a button in `FirstNode` that will run the ***tabbed navigation*** feature.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721628891943/4e4aeb95-5c4e-45d2-b39d-32559b4f2db2.gif align="center")

## Material3 Support

`Appyx` also provides [Material3](https://m3.material.io/) support with an out-of-the-box solution for bottom navigation, allowing developers to create a seamless and visually appealing navigation experience using built-in components. The material support library leverages the [spotlight](https://bumble-tech.github.io/appyx/components/spotlight/) component under the hood, which is a powerful tool for managing navigation transitions and animations. To utilize this feature, we need to add another dependency to the project. This additional dependency ensures that the spotlight component is available, enabling smooth integration and functionality of the bottom navigation within our application. By incorporating Material3 support, we can enhance the user interface and provide a more cohesive and modern look and feel to our app's navigation system.

```ini
appyx-utils-material = { module = "com.bumble.appyx:utils-material3", version.ref = "appyx" }
```

```kotlin
commonMain.dependencies {
    ...
    api(libs.appyx.utils.material)
}
```

After syncing the project, we will reach the `AppyxNavItem`, which will be used in bottom navigation, and the `AppyxMaterial3NavNode`, responsible for navigation. The creation of the `TabNavigationItems` is straightforward and similar to the previously used linear navigation. We need to create `destinations`; in our case, these will be the enums that represent the screens/nodes.

```ini
@Parcelize
enum class TabNavigationItems : Parcelable {
    FIRST_DESTINATION, SECOND_DESTINATION;
}
```

Now we can create the `resolver` that will be responsible for creating the bottom bar. It takes the defined ***destination*** and creates the proper navigation items, with ***text***, ***icons***, and a **lambda** that will create the desired ***nodes***.

```ini
companion object {
    val resolver: (TabNavigationItems) -> AppyxNavItem = { navBarItem ->
        when (navBarItem) {
            FIRST_DESTINATION -> AppyxNavItem(
                text = "Third",
                unselectedIcon = Icons.Sharp.Home,
                selectedIcon = Icons.Filled.Home,
                node = { ThirdNode(it) }
            )

            SECOND_DESTINATION -> AppyxNavItem(
                text = "Fourth",
                unselectedIcon = Icons.Sharp.AccountBox,
                selectedIcon = Icons.Filled.AccountBox,
                node = { FourthNode(it) }
            )
        }
    }
}
```

The last thing to do is to create proper starting node.

```ini
class TabNode(
    nodeContext: NodeContext,
) : AppyxMaterial3NavNode<TabNavigationItems>(
    nodeContext = nodeContext,
    navTargets = TabNavigationItems.entries,
    navTargetResolver = TabNavigationItems.resolver,
    initialActiveElement = TabNavigationItems.FIRST_DESTINATION,
)
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721629592185/a5dd9544-ec54-478d-8806-0d34169653f3.gif align="center")

# Coroutines

There is no official approach on how to handle coroutines inside the ***Nodes***, but there are several strategies we can employ. One common method is to utilize the ***NodeLifecycle***, which provides both the `lifecycle` and `lifecycleScope`. This allows us to manage coroutines effectively within the node's lifecycle, as shown in the example.

> The [Decompose](https://hashnode.com/post/clypbla0r000909l4gov25akn) (which I mentioned in a previous post) provides us with default support for coroutines inside the library core elements - Component.

Additionally, we can leverage the `lifecycle` to add an `observer` using `PlatformLifecycleEventObserver`. This observer can be used to create and manage a `coroutineScope`, ensuring that coroutines are properly handled throughout the node's lifecycle events.

Another option is to use the scope provided by the @Composable view with `rememberCoroutineScope()`. This approach ties the coroutine scope to the composable's lifecycle, making it a convenient choice for managing coroutines within composable functions.

Moreover, we can mix these approaches to suit different needs and scenarios. For instance, we might use `NodeLifecycle` for some coroutines and `rememberCoroutineScope()` for others, depending on the specific requirements of our application. By combining these strategies, we can achieve a robust and flexible coroutine management system within our nodes.

Moving further I will extend the `SecondNode` with a countdown timer that will be started on the screen creation and update the text value.

```ini
    private val countDownText = mutableStateOf<String>("0")

init {
    lifecycle.coroutineScope.launch {
        for (i in 10 downTo 0) {
            countDownText.value = i.toString()
            delay(1000)
        }
    }
}
```

```ini
@Composable
override fun Content(modifier: Modifier) {
    Column(...) {
        ...
        Spacer(modifier = Modifier.height(16.dp))
        Text("Countdown: ${countDownText.value}")
    }
}
```

You should consider finding a more suitable place to hold your business logic than the `Node`. Ideally, this place should be capable of handling configuration changes and recreating the view while retaining the state. The `ViewModel` is an excellent choice for this purpose, as it is specifically designed to manage UI-related data in a lifecycle-conscious way. However, it's important to note that `ViewModel` is not currently a part of the Appyx library, so you will need to implement it yourself.

Appyx is actively working on adding `ViewModel` support, and you can express your interest and vote for this feature [here](https://github.com/bumble-tech/appyx/issues/553). This will help prioritize its development and integration into the library.

In the meantime, if you need to ensure that your application survives configuration changes, there is an official [guide](https://bumble-tech.github.io/appyx/2.x/navigation/features/surviving-configuration-changes/) available. This guide provides detailed instructions and best practices for managing configuration changes effectively. By following this guide, you can ensure that your app remains robust and maintains its state across various configuration changes.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721630302507/6e6c65d9-33d7-4979-92da-619e1a62cc5b.gif align="center")

# Summary

The `Appyx` library is a powerful tool that allows you to create fully customized navigation in your Compose Multiplatform application. It's a great solution for creating complex navigation structures, and it's easy to use. The library is tightly coupled with Jetpack Compose but doesn't provide a dedicated component to hold your business logic, so you are free to use your own solutions. The library is still in the development phase, waiting for `ViewModel` support as mentioned in the post. Therefore, it doesn't provide out-of-the-box support for coroutines. You need to handle it yourself, but it's not a big deal.

I hope this post has given you a good overview of the `Appyx` library and how you can use it to create navigation in your Compose Multiplatform application. If you have any questions or comments, please feel free to leave them below. I'd love to hear from you!

> In this post, we'll explore the Appyx library for Compose Multiplatform, covering its integration, navigation capabilities, and support for various scenarios such as linear navigation, tab navigation, and asynchronous operations with coroutines. We'll walk through setting up dependencies, creating nodes, managing navigation, and handling coroutines. Additionally, we'll touch upon Appyx's flexibility for custom navigation solutions and its ongoing development for enhanced features like ViewModel support. This guide aims to provide a comprehensive understanding of utilizing Appyx for robust app navigation.
