Encapsulate your navigation code
Stay organized with collections Save and categorize content based on your preferences.

When using the Kotlin DSL to construct your graph, keeping destinations and navigation events in a single file can be difficult to maintain. This is especially true if you have multiple independent features.

Extract destinations

You should move your destinations into NavGraphBuilder extension functions. They should live close to the routes which define them, and the screens that they display. For example, consider the following app-level code that creates a destination which shows a list of contacts:

// MyApp.kt
@Serializable
objectContacts
@Composable
funMyApp(){
...
NavHost(navController,startDestination=Contacts){
composable<Contacts>{ContactsScreen(/* ... */)}
}
}

You should move the navigation-specific code into a separate file:

// ContactsNavigation.kt
@Serializable
objectContacts
funNavGraphBuilder.contactsDestination(){
composable<Contacts>{ContactsScreen(/* ... */)}
}
// MyApp.kt
@Composable
funMyApp(){
...
NavHost(navController,startDestination=Contacts){
contactsDestination()
}
}

The routes and destination definitions are now separate from the main app and you can update them independently. The main app is only dependent on a single extension function. In this case, that is NavGraphBuilder.contactsDestination().

The NavGraphBuilder extension function forms the bridge between a stateless screen-level composable function and Navigation-specific logic. This layer can also define where the state comes from and how you handle events.

Example

The following snippet introduces a new destination to display a contact's details, and updates the existing contact list destination to expose a navigation event to display the contact's details.

Here's a typical set of screens that can be internal to their own module, so that other modules cannot access them:

// ContactScreens.kt
// Displays a list of contacts
@Composable
internalfunContactsScreen(
uiState:ContactsUiState,
onNavigateToContactDetails:(contactId:String)->Unit
){...}
// Displays the details for an individual contact
@Composable
internalfunContactDetailsScreen(contact:ContactDetails){...}

Create destinations

The following NavGraphBuilder extension function creates a destination which shows the ContactsScreen composable. In addition, it now connects the screen with a ViewModel that provides the screen UI state and handles the screen-related business logic.

Navigation events, such as navigating to the contact details destination, are exposed to the caller rather than being handled by the ViewModel.

// ContactsNavigation.kt
@Serializable
objectContacts
// Adds contacts destination to `this` NavGraphBuilder
funNavGraphBuilder.contactsDestination(
// Navigation events are exposed to the caller to be handled at a higher level
onNavigateToContactDetails:(contactId:String)->Unit
){
composable<Contacts>{
// The ViewModel as a screen level state holder produces the screen
// UI state and handles business logic for the ConversationScreen
valviewModel:ContactsViewModel=hiltViewModel()
valuiState=viewModel.uiState.collectAsStateWithLifecycle()
ContactsScreen(
uiState,
onNavigateToContactDetails
)
}
}

You can use the same approach to create a destination which displays the ContactDetailsScreen. In this case, instead of obtaining the UI state from a view model, you can obtain it directly from the NavBackStackEntry.

// ContactsNavigation.kt
@Serializable
internaldataclassContactDetails(valid:String)
funNavGraphBuilder.contactDetailsScreen(){
composable<ContactDetails>{navBackStackEntry->
ContactDetailsScreen(contact=navBackStackEntry.toRoute())
}
}

Encapsulate navigation events

In the same way that you encapsulate destinations, you can encapsulate navigation events to avoid exposing route types unnecessarily. Do this by creating extension functions on NavController.

// ContactsNavigation.kt
funNavController.navigateToContactDetails(id:String){
navigate(route=ContactDetails(id=id))
}

Bring it together

The navigation code for displaying contacts is now cleanly separated from the app's navigation graph. The app needs to:

  • Call NavGraphBuilder extension functions to create destinations
  • Connect those destinations by calling NavController extension functions for navigation events
// MyApp.kt
@Composable
funMyApp(){
...
NavHost(navController,startDestination=Contacts){
contactsDestination(onNavigateToContactDetails={contactId->
navController.navigateToContactDetails(id=contactId)
})
contactDetailsDestination()
}
}

In summary

  • Encapsulate your navigation code for a related set of screens by placing it in a separate file
  • Expose destinations by creating extension functions on NavGraphBuilder
  • Expose navigation events by creating extension functions on NavController
  • Use internal to keep screens and route types private

Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2025年02月10日 UTC.