Building modern Android applications often requires slicing up the user experience into distinct screens, each representing a unique state of the app. These screens are connected through a navigation system, allowing users to move seamlessly between different parts of the application.
A crucial aspect of this navigation process is the ability to pass relevant data from one screen to another. For example, when navigating from a list of items to the details of a specific item, we need to pass the item’s identifier (such as an ID or a unique identifier) to the details screen. This ensures that the correct information is displayed, enhancing the overall user experience and the app’s functionality. Historically, managing navigation in Android apps has involved a considerable amount of boilerplate code and type-checking.
Developers had to manually construct navigation routes using string-based URIs and handle the extraction and type-checking of arguments in the destination screens.
However, the recent introduction of Compose as a modern UI framework for Android, along with the introduction of Type-Safe navigation in Compose Navigation 2.8.0, has transformed the way we can build and optimize UIs, without having to worry about the compatibility of types between screens.
In this blog post, we’ll explore the power of Type-Safe navigation in Compose and how it can streamline the development of your Android applications. We’ll first look at the challenges faced with navigation before the introduction of this feature, and then dive into the benefits and implementation details of Type-Safe navigation.
Imagine we have two screens in our app - a HomeScreen and a PostDetailsScreen. From the HomeScreen, we want to navigate to the PostDetailsScreen and pass along a post ID to retrieve the corresponding details.
Let’s define an object to centralize the routes and the arguments in the app and not handle them as string values directly in the navigation graph:
package co.wawand.composetypesafenavigation.presentation.navigation
import co.wawand.composetypesafenavigation.presentation.navigation.NavArguments.POST_ID
object NavDestinations {
const val WELCOME_ROUTE = "welcome"
const val HOME_ROUTE = "home"
const val LOADING_ROUTE = "loading"
private const val POST_DETAILS = "post_details"
const val POST_DETAILS_ROUTE = "$POST_DETAILS/{$POST_ID}"
// Helper function to create the route with parameter
fun postDetailsRoute(postId: Long) = "$POST_DETAILS/$postId"
}
object NavArguments {
const val POST_ID = "postId"
}
Also, we could centralize the navigation by defining a sealed class to represent all the possible navigation events belonging to the app and add a function to handle those events:
package co.wawand.composetypesafenavigation.presentation.navigation
import androidx.navigation.NavController
import co.wawand.composetypesafenavigation.presentation.navigation.NavDestinations.HOME_ROUTE
sealed class NavigationEvent {
data object OnNavigateUp : NavigationEvent()
data object NavigateToHome : NavigationEvent()
data class OnNavigateToPostDetails(val postId: Long) : NavigationEvent()
}
fun handleNavigationEvent(navController: NavController, event: NavigationEvent) {
when (event) {
is NavigationEvent.OnNavigateUp -> navController::navigateUp
is NavigationEvent.NavigateToHome -> {
navController.navigate(HOME_ROUTE) {
popUpTo(HOME_ROUTE) { inclusive = true }
}
}
is NavigationEvent.OnNavigateToPostDetails -> {
navController.navigate(NavDestinations.postDetailsRoute(event.postId))
}
}
}
Now, let’s define the navigation graph and how to link the HomeScreen to the PostDetailsScreen:
package co.wawand.composetypesafenavigation.presentation.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
@Composable
fun AppNavigation(navController: NavHostController) {
NavHost(navController = navController, startDestination = NavDestinations.HOME_ROUTE) {
composable(route = NavDestinations.HOME_ROUTE) {
HomeScreen(
onGoToDetails = { postId -> NavigationEvent.OnNavigateToPostDetails(postId) },
// more parameters
)
}
composable(
route = NavDestinations.POST_DETAILS_ROUTE,
arguments = listOf(
navArgument(NavArguments.POST_ID) {
type = NavType.LongType
}
)
) { navBackStackEntry ->
val postId = navBackStackEntry.arguments?.getLong(NavArguments.POST_ID) ?: 0L
PostDetailsScreen(
postId = postId,
// more parameters
)
}
}
}
In this example, we’re using the navigate() function from the Navigation component to pass the postId as part of the navigation route. On the PostDetailsScreen, we then need to retrieve this value from the NavBackStackEntry and handle the potential for a null value.
This approach introduces several challenges:
PostDetailsScreen implementation needs to handle the potential for a null value when retrieving the postId argument, adding extra complexity to the code.To address these issues, the Compose Navigation 2.8.0 release introduced Type-Safe navigation, let’s explore it and refresh the previous code to use it.
The new type-safe navigation APIs for Kotlin allow you to use any serializable type to define navigation destinations. To use them we need to
Compose Navigation dependency to at least the 2.8.0 version.Once done, and after synchronizing the dependencies and the plugins required, we can use the @Serializable annotation to automatically create serializable types. Let’s see how to implement it:
package co.wawand.composetypesafenavigation.presentation.navigation
import kotlinx.serialization.Serializable
sealed class NavDestinations {
@Serializable
data object Welcome : AppDestinations()
@Serializable
data object Home : AppDestinations()
@Serializable
data object Loading : AppDestinations()
@Serializable
data class PostDetail(val postId: Long) : AppDestinations()
}
The previous NavDestinations object is now a sealed class that contains all the app destinations, which are objects or data classes serializable. The data classes allow us to establish parameters, with the types required.
Now, let’s adjust the function to handle the navigation event to use the proper route definition:
fun handleNavigationEvent(navController: NavController, event: NavigationEvent) {
when (event) {
is NavigationEvent.OnNavigateUp -> navController.navigateUp()
is NavigationEvent.NavigateToHome -> navController.navigate(AppDestinations.Home) {
popUpTo(navController.graph.startDestinationId) { inclusive = true }
}
is NavigationEvent.OnNavigateToPostDetails -> navController.navigate(
AppDestinations.PostDetail(event.postId)
)
}
}
And now, let’s dive into the navigation graph, where the magic appears:
package co.wawand.composetypesafenavigation.presentation.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
@Composable
fun AppNavigation(navController: NavHostController) {
NavHost(navController = navController, startDestination = NavDestinations.Home) {
composable<NavDestinations.Home> {
HomeScreen(
onGoToDetails = { postId -> NavigationEvent.OnNavigateToPostDetails(postId) },
// more parameters
)
}
composable<NavDestinations.PostDetail>{ navBackStackEntry ->
val postDetail = navBackStackEntry.toRoute<NavDestinations.PostDetail>()
PostDetailsScreen(
postId = postDetail.postId,
// more parameters
)
}
}
}
And that’s it! Easy, right?
With the introduction of Type-Safe navigation, the process of navigating between screens and passing arguments becomes much more streamlined and robust. Here’s how it works:
This new approach to navigation in Compose represents a significant leap forward in terms of code quality and developer experience. By leveraging the power of Kotlin’s type system and sealed classes, we can create a more structured and maintainable navigation system. The transformation from string-based routes to type-safe navigation not only reduces the potential for errors but also provides a more intuitive way to manage the flow between screens in your application.
Let’s explore the key benefits that make Type-Safe navigation a game-changer for Android development with Compose:
Overall, the introduction of Type-Safe navigation in Compose 2.8.0 has not only streamlined the development process but has also elevated the quality and robustness of Android applications. By embracing this feature, you can build more intuitive, user-friendly, and maintainable apps that provide a seamless navigation experience for your users
As we continue to develop the Android applications with Compose, we can explore the power of Type-Safe navigation and unlock the full potential of this modern UI framework. With the improved developer experience and the elimination of common navigation-related issues, we can focus more on creating exceptional user experiences and delivering high-quality applications.