How Jetpack Compose Navigation Works in the Muse App: Dashboard to Editor Flow
Jetpack Compose Navigation in the Muse app uses a centralized NavHost in MainScreen.kt with typed, serializable route arguments to enable compile-time-safe navigation between the Dashboard and Editor screens.
The Muse app 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, 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. This file creates a NavHostController via rememberNavController() and supplies it through LocalNavController (defined in 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:
// 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:
- DashboardArgs: An object representing the root destination with no parameters
- EditorArgs: A data class carrying the
scriptIdstring needed to load specific content
These routes are defined as serializable classes:
// 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 (located at 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:
// DashboardScreen.kt (lines 64-68)
ScriptItem(
modifier = Modifier.clickable { onLaunchEditor(script) },
// ...
)
The actual navigation logic resides in 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 at muse/src/commonMain/kotlin/io/github/kkoshin/muse/feature/editor/EditorScreen.kt, the destination declaration handles argument extraction:
// 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 (lines 104–108), the Export screen callback demonstrates this pattern:
// 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 declares abstract navigation functions that Android and iOS implement separately:
// 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, 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.ktusing type-safecomposable<Arg>blocks - Serializable Routes:
DashboardArgsandEditorArgsprovide 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:
expectfunctions 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 using rememberNavController(). Child composables access navigation actions through 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, the screen invokes the onLaunchEditor callback provided by the parent. The implementation in 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 (lines 104–108).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →