# How Jetpack Compose Navigation Works in the Muse App: Dashboard to Editor Flow

> Explore Jetpack Compose navigation in the Muse app. Learn how typed, serializable routes in NavHost connect Dashboard and Editor screens with compile-time safety.

- Repository: [Ko Shin/muse](https://github.com/kkoshin/muse)
- Tags: deep-dive
- Published: 2026-03-05

---

**Jetpack Compose Navigation in the Muse app uses a centralized `NavHost` in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt) with typed, serializable route arguments to enable compile-time-safe navigation between the Dashboard and Editor screens.**

The [Muse app](https://github.com/kkoshin/muse) implements type-safe navigation across its core UI modules using Jetpack Compose Navigation. By defining sealed route arguments and centralizing the navigation graph in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt), the app ensures that navigation state and parameters remain type-safe when moving between screens.

## Centralizing the Navigation Graph in MainScreen.kt

The navigation architecture centers on a single `NavHost` declared in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt). This file creates a `NavHostController` via `rememberNavController()` and supplies it through `LocalNavController` (defined in [`LocalNavController.kt`](https://github.com/kkoshin/muse/blob/main/LocalNavController.kt)) for child composables to access navigation actions.

The `NavHost` defines `DashboardArgs` as the start destination and registers each screen using the `composable<Arg>` DSL. According to the source code, the graph setup spans lines 38–81 in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt):

```kotlin
// MainScreen.kt (lines 38-53, 54-81)
@Composable
fun MainScreen(navController: NavHostController = rememberNavController()) {
    // ...
    NavHost(
        navController = navController,
        startDestination = DashboardArgs,
    ) {
        composable<DashboardArgs> { _ -> 
            DashboardScreen(onLaunchEditor = { script ->
                navController.navigate(
                    EditorArgs(scriptId = script.id.toString())
                )
            })
        }
        composable<EditorArgs> { entry ->
            val args = entry.toRoute<EditorArgs>()
            EditorScreen(args = args, /* ... */)
        }
        // Additional routes...
    }
}

```

This declarative approach ensures that every destination is explicitly typed and instantiated with the correct arguments.

## Defining Type-Safe Route Arguments

Muse uses Kotlin's **serialization** to define route arguments, eliminating string-based routing errors. Two primary routes handle the Dashboard-to-Editor flow:

1. **DashboardArgs**: An object representing the root destination with no parameters
2. **EditorArgs**: A data class carrying the `scriptId` string needed to load specific content

These routes are defined as serializable classes:

```kotlin
// Defined within MainScreen.kt and EditorScreen.kt
@Serializable
object DashboardArgs

@Serializable
data class EditorArgs(val scriptId: String)

```

When `navController.navigate()` receives an `EditorArgs` instance, the Navigation Compose library automatically serializes the arguments into the back stack entry.

## Triggering Navigation from DashboardScreen

Inside [`DashboardScreen.kt`](https://github.com/kkoshin/muse/blob/main/DashboardScreen.kt) (located at [`muse/src/commonMain/kotlin/io/github/kkoshin/muse/feature/dashboard/DashboardScreen.kt`](https://github.com/kkoshin/muse/blob/main/muse/src/commonMain/kotlin/io/github/kkoshin/muse/feature/dashboard/DashboardScreen.kt)), each script item in the list triggers navigation through a click handler. The UI layer remains agnostic of navigation implementation, merely invoking a callback:

```kotlin
// DashboardScreen.kt (lines 64-68)
ScriptItem(
    modifier = Modifier.clickable { onLaunchEditor(script) },
    // ...
)

```

The actual navigation logic resides in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt) where the `onLaunchEditor` lambda constructs the `EditorArgs` and calls `navController.navigate()`. This separation keeps UI components pure while centralizing routing logic in the navigation host.

## Receiving Arguments in EditorScreen.kt

The `EditorScreen` composable extracts typed arguments from the `NavBackStackEntry` using the `toRoute<T>()` extension function. Located in [`EditorScreen.kt`](https://github.com/kkoshin/muse/blob/main/EditorScreen.kt) at [`muse/src/commonMain/kotlin/io/github/kkoshin/muse/feature/editor/EditorScreen.kt`](https://github.com/kkoshin/muse/blob/main/muse/src/commonMain/kotlin/io/github/kkoshin/muse/feature/editor/EditorScreen.kt), the destination declaration handles argument extraction:

```kotlin
// EditorScreen.kt (lines 47-53, 58-61)
@Serializable
data class EditorArgs(val scriptId: String)

@Composable
fun EditorScreen(
    args: EditorArgs,
    // ...
) {
    // args.scriptId contains the ID passed from Dashboard
    val scriptId = args.scriptId
}

```

This pattern ensures that `EditorScreen` receives compile-time-safe data without manual string key lookups or Bundle parsing.

## Managing the Back Stack with popBackStack

After completing tasks in the Editor (such as exporting audio), the app returns to the Dashboard using `popBackStack()`. This method removes the Editor from the navigation stack while preserving the Dashboard's state. In [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt) (lines 104–108), the Export screen callback demonstrates this pattern:

```kotlin
// MainScreen.kt export completion handler
navController.popBackStack(DashboardArgs, false)

```

The `false` parameter indicates that the Dashboard itself should not be popped—only destinations above it in the stack are removed. This maintains the user's scroll position and state in the Dashboard when returning from the Editor.

## Extending Navigation with Platform-Specific Routes

Muse supports platform-specific functionality through Kotlin's `expect`/`actual` mechanism. The common source in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt) declares abstract navigation functions that Android and iOS implement separately:

```kotlin
// MainScreen.kt (lines 73-77)
expect fun NavGraphBuilder.addPlatformSpecificRoutes(navController: NavHostController)

expect fun onLaunchAudioIsolation(navController: NavHostController, path: okio.Path)

```

Each platform source set (e.g., [`MainScreen.android.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.android.kt), [`MainScreen.ios.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.ios.kt)) provides concrete implementations for features like audio isolation or open-source license pages. This structure keeps the core navigation graph portable while allowing OS-specific extensions without polluting the shared code.

## Summary

- **Centralized NavHost**: All routes register in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt) using type-safe `composable<Arg>` blocks
- **Serializable Routes**: `DashboardArgs` and `EditorArgs` provide compile-time safety for navigation arguments
- **Argument Extraction**: Destinations use `entry.toRoute<EditorArgs>()` to retrieve typed parameters from the back stack entry
- **Back Stack Control**: `navController.popBackStack(DashboardArgs, false)` returns to previous screens while preserving their state
- **Platform Extensions**: `expect` functions allow Android and iOS to inject specific routes without modifying the core graph

## Frequently Asked Questions

### How does type safety work in Muse's Compose Navigation?

The app's navigation uses Kotlin Serialization with `@Serializable` annotated classes (`DashboardArgs`, `EditorArgs`). When calling `navController.navigate(EditorArgs(scriptId))`, the Navigation Compose library serializes the object into the back stack. The destination extracts it using `entry.toRoute<EditorArgs>()`, ensuring the `scriptId` property is available as a typed `String` rather than requiring manual Bundle key lookups.

### Where is the NavController stored and accessed in the Muse app?

The `NavHostController` is created in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt) using `rememberNavController()`. Child composables access navigation actions through [`LocalNavController.kt`](https://github.com/kkoshin/muse/blob/main/LocalNavController.kt), which provides a `LocalNavigationController` composition local. This pattern avoids passing the controller through multiple composable layers while maintaining testability.

### How does the Dashboard pass data to the Editor screen?

When a user clicks a script item in [`DashboardScreen.kt`](https://github.com/kkoshin/muse/blob/main/DashboardScreen.kt), the screen invokes the `onLaunchEditor` callback provided by the parent. The implementation in [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt) constructs an `EditorArgs` instance containing the script's ID as a string, then calls `navController.navigate()` with that object. The Editor receives the ID through the typed `args: EditorArgs` parameter.

### Can I navigate back to the Dashboard from the Editor without losing state?

Yes. The Muse app uses `navController.popBackStack(DashboardArgs, false)` in the Editor's completion handlers. The `false` argument ensures the Dashboard destination remains on the stack (and its state is preserved) while removing the Editor above it. This is implemented in the Export screen callback within [`MainScreen.kt`](https://github.com/kkoshin/muse/blob/main/MainScreen.kt) (lines 104–108).