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