Navigation in Jetpack Compose

In this article I will cover 2 methods

  1. Navigate directly from the screen
  2. Navigate with the ViewModel (Advanced)

Setup

Add these dependencies to your app build.gradle file.

dependencies {
def nav_version = "2.5.3"

implementation "androidx.navigation:navigation-compose:$nav_version"
}

Set up the navigation

Let’s create a Navigation.kt file in which we’ll set up the navigation.

@Composable
fun Navigation() {
val navController = rememberNavController()

NavHost(
navController = navController,
// We need to create the routes first
startDestination = ""
) {
/* ... */
}
}

NavController — Allows us to navigate to other screens Keeps track of the back stack of the composable.

NavHost — links the NavController with a navigation graph that specifies the composable destinations that you should be able to navigate between screens.

Creating routes

There are different ways to create your routes, but I’ll show you the one that I prefer. Firstly I create a file named Screen.kt and I store them inside a sealed class.

sealed class Screen(val route: String) {
object Home : Screen(route = "home")
object Favorites : Screen(route = "favorites")
object Cart : Screen(route = "cart")
}

Adding screens to NavHost

@Composable
fun Navigation() {
val navController = rememberNavController()

NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(route = Screen.Home.route) {
/* ... */
}
composable(route = Screen.Favorites.route) {
/* ... */
}
composable(route = Screen.Cart.route) {
/* ... */
}
}
}

Let’s create the screens.

@Composable
fun HomeScreen(
navigate: () -> Unit
) {
Column {
Text(text = "Home")
Button(onClick = navigate) {
Text(text = "Navigate to favorites")
}
}
}

@Composable
fun FavoritesScreen(
navigateBack: () -> Unit,
navigate: () -> Unit
) {
Column {
Text(text = "Favorites")
Button(onClick = navigateBack) {
Text(text = "Navigate back")
}
Button(onClick = navigate) {
Text(text = "Navigate to cart")
}
}
}

@Composable
fun CartScreen(navigateBackToHome: () -> Unit) {
Column {
Text(text = "Cart")
Button(onClick = navigateBackToHome) {
Text(text = "Navigate back to home")
}
}
}

It is recommended to not share the NavController through screens, because you should only call navigate() as part of a callback and not as part of your composable itself, to avoid calling navigate() on every recomposition.

@Composable
fun Navigation() {
val navController = rememberNavController()

NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(route = Screen.Home.route) {
HomeScreen(
navigate = {
navController.navigate(Screen.Favorites.route) {
// If it is true, multiple copies won't be created
launchSingleTop = true
}
}
)
}
composable(route = Screen.Favorites.route) {
FavoritesScreen(
navigateBack = {
// Navigate back
navController.popBackStack()
},
navigate = {
navController.navigate(Screen.Cart.route) {
launchSingleTop = true
}
}
)
}
composable(route = Screen.Cart.route) {
CartScreen(
navigateBackToHome = {
// Navigate back to Home screen
navController.popBackStack(
route = Screen.Home.route,
// If this is true, the destination will be popped
inclusive = false
)
}
)
}
}
}

Navigate using the ViewModel

If the UI is complex you should use this method.

Setup

Add these dependencies to your app build.gradle file.

dependencies { 
def lifecycle_version = "2.6.0-alpha03"

// Pass the ViewModel directly to the screen
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
}

Create a sealed interface named UiEvent.kt, here are our app events.

sealed interface UiEvent {
data class Navigate(val route: String): UiEvent
data class PopBackStack(val route: String? = null, val inclusive: Boolean = false): UiEvent
}

Let’s analyze our screens, ViewModels and events.

// Screen events
sealed interface HomeScreenEvents {
object OnNavigateToFavorites : HomeScreenEvents
}

class HomeViewModel : ViewModel() {

// Creating a channel within which we send events
private val _uiEvent: Channel<UiEvent> = Channel()
// Convert the received values into a flow to collect it's data
val uiEvent = _uiEvent.receiveAsFlow()

// Helper function to remove the boilerplate code
private fun sendEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}

fun onEvent(event: HomeScreenEvents) {
// We have multiple action cases
when (event) {
HomeScreenEvents.OnNavigateToFavorites -> {
// Sends an UiEvent of type Navigation with a route in it
sendEvent(UiEvent.Navigate(route = Screen.Favorites.route))
}
}
}
}

@Composable
fun HomeScreen(
navigate: (UiEvent.Navigate) -> Unit,
// ViewModel is passed by the lifecycle
viewModel: HomeViewModel = viewModel()
) {
LaunchedEffect(key1 = true) {
// Collects the latest event from the channel
viewModel.uiEvent.collectLatest { event ->
when (event) {
is UiEvent.Navigate -> {
navigate(event)
}

else -> Unit
}
}
}

Column {
Text(text = "Home")
Button(
onClick = {
// Sending to the onEvent function an event
viewModel.onEvent(HomeScreenEvents.OnNavigateToFavorites)
}
) {
Text(text = "Navigate to favorites")
}
}
}
// Screen events
sealed interface FavoritesScreenEvents {
object OnNavigateToCart : FavoritesScreenEvents
object OnNavigateBack : FavoritesScreenEvents
}

class FavoritesViewModel : ViewModel() {

// Creating a channel within which we send events
private val _uiEvent: Channel<UiEvent> = Channel()
// Convert the received values into a flow to collect it's data
val uiEvent = _uiEvent.receiveAsFlow()

// Helper function to remove the boilerplate code
private fun sendEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}

fun onEvent(event: FavoritesScreenEvents) {
when (event) {
FavoritesScreenEvents.OnNavigateBack -> {
// Sends an UiEvent to navigate back
sendEvent(UiEvent.PopBackStack())
}

FavoritesScreenEvents.OnNavigateToCart -> {
// Sends an UiEvent to navigate to cart screen
sendEvent(UiEvent.Navigate(route = Screen.Cart.route))
}
}
}
}

@Composable
fun FavoritesScreen(
// ViewModel is passed by the lifecycle
viewModel: FavoritesViewModel = viewModel(),
navigateBack: () -> Unit,
navigate: (UiEvent.Navigate) -> Unit
) {
LaunchedEffect(key1 = true) {
// Collects the latest event from the channel
viewModel.uiEvent.collectLatest { event ->
when (event) {
is UiEvent.Navigate -> {
navigate(event)
}

is UiEvent.PopBackStack -> {
navigateBack()
}
}
}
}

Column {
Text(text = "Favorites")
Button(
onClick = {
viewModel.onEvent(FavoritesScreenEvents.OnNavigateBack)
}
) {
Text(text = "Navigate back")
}
Button(
onClick = {
viewModel.onEvent(FavoritesScreenEvents.OnNavigateToCart)
}
) {
Text(text = "Navigate to cart")
}
}
}
// Screen events
sealed interface CartScreenEvents {
object OnNavigateBackToHome : CartScreenEvents
}

class CartViewModel : ViewModel() {

// Creating a channel within which we send events
private val _uiEvent: Channel<UiEvent> = Channel()
// Convert the received values into a flow to collect it's data
val uiEvent = _uiEvent.receiveAsFlow()

// Helper function to remove the boilerplate code
private fun sendEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}

fun onEvent(event: CartScreenEvents) {
when (event) {
CartScreenEvents.OnNavigateBackToHome -> {
sendEvent(
UiEvent.PopBackStack(
// Pop to this route
route = Screen.Home.route,
// If this is true, the destination will be popped
inclusive = false
)
)
}
}
}
}

@Composable
fun CartScreen(
// ViewModel is passed by the lifecycle
viewModel: CartViewModel = viewModel(),
navigateBack: (UiEvent.PopBackStack) -> Unit
) {
LaunchedEffect(key1 = true) {
// Collects the latest event from the channel
viewModel.uiEvent.collectLatest { event ->
when (event) {
is UiEvent.PopBackStack -> {
navigateBack(event)
}

else -> Unit
}
}
}
Column {
Text(text = "Cart")
Button(
onClick = {
viewModel.onEvent(CartScreenEvents.OnNavigateBackToHome)
}
) {
Text(text = "Navigate back to home")
}
}
}

LaunchedEffect — is a Side-effect, if you want to learn more about it and other Side-effects, I have an article about them.

@Composable
fun Navigation() {
val navController = rememberNavController()

NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(route = Screen.Home.route) {
HomeScreen(
navigate = { destination ->
navController.navigate(destination.route) {
launchSingleTop = true
}
}
)
}
composable(route = Screen.Favorites.route) {
FavoritesScreen(
navigateBack = {
navController.popBackStack()
},
navigate = { destination ->
navController.navigate(destination.route) {
launchSingleTop = true
}
}
)
}
composable(route = Screen.Cart.route) {
CartScreen(
navigateBack = { destination ->
// In a bigger project you can either
// pop up to a specific screen or to the previous screen
if (destination.route == null) {
navController.popBackStack()
} else {
navController.popBackStack(
route = destination.route,
inclusive = destination.inclusive
)
}
}
)
}
}
}

I hope that you found this article helpful. Follow me if you want to see more articles like this one.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store