Before Navigation version 2.8.0 setup we define every destination as a raw string, navArgument() everywhere and then manually parse bundles in your ViewModels.
Over time this leads to:
- Crashes – route not found: movie‑detail/123
- Boilerplate overload – string constants, key constants,
navArgumentlists, bundle parsing - Poor refactorability – rename a route and you’ve got to hunt down every usage
With AndroidX Navigation 2.8.0 and above you get a type‑safe Kotlin DSL that turns each screen route into a real Kotlin type, no more string literals, no more unsafe casts and no extra Gradle plugins for Safe‑Args. Below we’ll step through migrating android app to this new approach and explain how it all fits together.
Upgrading your dependencies above or 2.8.0
implementation "androidx.navigation:navigation-compose:2.8.0"
1. Defining your route types
Before :
object AppDestinations {
// no‑arg screens
const val HOME_ROUTE = "home"
// arg‑taking screen
const val SEARCH_ROUTE = "search"
const val SEARCH_TYPE = "searchType"
// arg‑taking screen
const val MOVIE_DETAILS_ROUTE = "movie-detail"
const val MOVIE_DETAILS_ID_KEY = "movieId"
}
Every destination is a String. Arguments live in the route template ("search/{searchType}") and you manually define them with navArgument(…).
After : A sealed, serializable hierarchy
@Serializable
sealed interface AppDestinations {
@Serializable
data object Home : AppDestinations
@Serializable
data class Search(val searchType: SearchType) : AppDestinations
@Serializable
data class MovieDetail(val movieId: Int) : AppDestinations
}
- object for zero‑arg routes
- data class for routes carrying any parameters
- Everything annotated @Serializable so Kotlin Serialization can (de)serialize your route into a Bundle
2. Rewriting Your NavHost
Before : string‑based NavHost
NavHost(
navController = navController,
startDestination = AppDestinations.HOME_ROUTE,
) {
composable(route = AppDestinations.HOME_ROUTE) {
HomeScreen()
}
composable(
route = "${AppDestinations.SEARCH_ROUTE}/{${routes.SEARCH_TYPE}}",
arguments = listOf(
navArgument(routes.SEARCH_TYPE) { type = NavType.EnumType(SearchType::class.java) },
),
) {
SearchScreen()
}
composable(
route = "${AppDestinations.MOVIE_DETAILS_ROUTE}/{${routes.MOVIE_DETAILS_ID_KEY}}",
arguments = listOf(
navArgument(routes.MOVIE_DETAILS_ID_KEY) { type = NavType.IntType },
),
) {
MovieDetailScreen()
}
}
After : type‑safe DSL NavHost
NavHost(
navController = navController,
startDestination = startDestination,
) {
composable<AppDestinations.Home> {
HomeScreen()
}
composable<AppDestinations.Search> {
SearchScreen()
}
composable<AppDestinations.MovieDetail> {
MovieDetailScreen()
}
}
- No more string templates and objects instead
- Automatic arg definition: the DSL infers
navArgumentfromSearch(val searchType: SearchType)
3. Refactoring Navigation actions
Before: URL builders
class NavigationActions(
private val navController: NavHostController,
) {
val navigateToSelectedMovie: (Int) -> Unit = { movieId: Int ->
navController.navigate("${AppDestinations.MOVIE_DETAILS_ROUTE}/$movieId")
}
val navigateToSearch: (SearchType) -> Unit = { searchType: SearchType ->
navController.navigate("${AppDestinations.SEARCH_ROUTE}/$searchType")
}
val navigateUp: () -> Unit = {
navController.navigateUp()
}
}
After: sealed helper lambda
class NavigationActions(
private val navController: NavHostController,
) {
val navigateToSelectedMovie: (Int) -> Unit = { movieId: Int ->
navController.navigate(AppDestinations.ActorDetail(movieId))
}
val navigateToSearch: (SearchType) -> Unit = { searchType: SearchType ->
navController.navigate(AppDestinations.Search(searchType))
}
val navigateUp: () -> Unit = {
navController.navigateUp()
}
}
All your navigation calls are simple one‑liner passing a Kotlin object.
4. ViewModel Integration – receive args from SavedStateHandle
Before: manual bundle parsing
@HiltViewModel
class SearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val searchRepository: SearchRepository,
) : ViewModel() {
val searchType: SearchType = checkNotNull(savedStateHandle[SEARCH_TYPE])
}
After : reconstruct route object
@HiltViewModel
class SearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val searchRepository: SearchRepository,
) : ViewModel() {
val searchType: SearchType = savedStateHandle.toRoute<AppDestinations.Search>().searchType
}
- The DSL writes your args into the
SavedStateHandlebundle toRoute<T>()applies Kotlin Serialization to recreate your route type
5. Migration – Incremental & Hybrid Strategies
Mixed XML + Compose :
If you have an XML‑defined NavHostFragment, add the Compose DSL via the new fragment adapter.
<fragment
android:id="@+id/composeNavHost"
android:name="androidx.navigation.compose.ComposableNavHostFragment"
app:navGraph="@navigation/main_graph" />
navController.addGraph(/* Compose‐DSL built graph */)
This lets you migrate one feature at a time. Pick routes for migration and migrate only those routes to the DSL. Verify end‑to‑end with UI tests and gradually continue for all destinations.
6. How the Kotlin DSL Simplifies Navigation Under the Hood
While it looks like you’re just swapping strings for objects, the DSL hides a lot behind a clean, type‑safe API. Here’s what’s happening internally in our Navigation example
inline fun <reified T : Any> NavGraphBuilder.composable(
typeMap: Map<KType, NavType<*>> = emptyMap(),
noinline content: @Composable (NavBackStackEntry) -> Unit
) { }
- We get a new
composable<T>()call simply by importing the navigation compose package. - Internally this calls into
ComposeNavigatorDestinationBuilder(provider[…], T::class, typeMap, content), which registers a destination whose route pattern and arguments are derived fromT’s@Serializablemetadata.
Conclusion :
Migrating to Navigation 2.8.0’s type‑safe Kotlin DSL:
- Eliminates string‑based routes
- Provides compile‑time safety and better refactoring
- Automatically handles Safe‑Args via Kotlin Serialization
- Keeps all destinations centralized in one sealed hierarchy
- Simplifies ViewModel arg retrievals
Resources :
Documentation – Type safety in Kotlin DSL and Navigation Compose
Jetpack Compose Navigation example – list to detail
Pull request with migration changes
Author

Raj
Hi ! I’m a Software Engineer at MedKitDoc & Technical writer.
I lead organization Developers Breach focusing on contributing to Open Source and Student Projects.
