Navigation with Jetpack Compose
Navigation is a method or technique used to allow users to move between different parts of an application. In Jetpack Compose, the navigation component helps manage and implement this navigation, ensuring a consistent and efficient user experience.
What we need?
To use navigation in our Android App we need to add compose dependency
The latest navigation compose dependency is
implementation("androidx.navigation:navigation-compose:2.7.7")
Step 1. in build.gradle.kts(Module: app)
implementation(libs.androidx.navigation.compose)
and
Step2. libs.version.toml(Version Catalog)
[versions]
navigationCompose = "2.7.7"
[libraries]
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
Key Components of Navigation in Jetpack Compose
1. NavController:
Purpose:
Central component that keeps track of the current screen and manages the back stack.
Usage: Created using rememberNavController() and passed to other composables to perform navigation actions.
2. NavHost:
Purpose: Hosts the navigation graph and manages the navigation destinations.
Usage: Defines the start destination and all possible composable destinations.
3. Composable Destinations:
Purpose: Individual screens or destinations within the navigation graph.
Usage: Defined using composable() within the NavHost.
4. Routes:
Purpose: String identifiers used to navigate between composables.
Usage: Typically represented as constants or sealed classes.
To determine which approach is best for your navigation in Jetpack Compose, consider the nature and complexity of your app’s screens and navigation requirements. Here’s a guide to help you choose between 'object', 'sealed class' or 'data class' for managing your screen routes and drawer items.
1. Use object when:
Static Screens: Your screens are static with no dynamic parameters.
Singleton Routes: Each route corresponds to a unique, immutable screen.
Simplicity: You prefer a straightforward and minimal approach without the need for complex navigation.
Object Screens
object Screens {
const val Main = "MainScreen"
const val Profile = "ProfileScreen"
const val Settings = "SettingsScreen"
}
Pros:
Simple and easy to manage.
Efficient for static routes.
Cons:
Limited flexibility for passing parameters.
Not suitable for hierarchical or complex navigation.
Implementation Example:
val screens = listOf(
Screens.Main to "Main Screen",
Screens.Profile to "Profile Screen",
Screens.Settings to "Settings Screen"
)
2. Use sealed class when:
Fixed Set of Screens: You have a fixed number of screens that you won’t need to modify frequently.
Type Safety: You want to ensure all possible destinations are accounted for.
Hierarchical Structure: Your screens might need to share common properties or logic.
Example:
sealed class
sealed class Screen(val route: String, val title: String) {
object Main : Screen("MainScreen", "Main Screen")
object Profile : Screen("ProfileScreen", "Profile Screen")
object Settings : Screen("SettingsScreen", "Settings Screen")
}
Pros:
Type-safe and exhaustive when used in when statements.
Supports inheritance for shared properties.
Cons:
Less flexibility for dynamic parameters.
More boilerplate if you have many screens.
Implementation Example:
val screens = listOf(Screen.Main, Screen.Profile, Screen.Settings)
3. Use data class when:
Dynamic Parameters: Your screens require different parameters that may change at runtime.
Parameter Passing: You need to pass complex data between screens.
Instance Management: You may need to create or compare instances of screens.
Example:
Object Screen
data class Screen(val route: String, val title: String)
object Screens {
val Main = Screen("MainScreen", "Main Screen")
val Profile = Screen("ProfileScreen", "Profile Screen")
val Settings = Screen("SettingsScreen", "Settings Screen")
}
Pros:
Flexible for passing and managing parameters.
Easy to create and copy instances with different values.
Cons:
May be overkill for static routes without parameters.
Slightly more complex setup.
Implementation Example:
val screens = listOf(Screens.Main, Screens.Profile, Screens.Settings)
Simple Example: Navigating Between Screens
MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
val navController = rememberNavController()
NavigationGraph(navController)
}
}
}
}
SealedClass
sealed class Screen(val route: String) {
object Home : Screen("home")
object Details : Screen("details")
}
HomeScreen
@Composable
fun HomeScreen(navController: NavHostController) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Home Screen - Click to go to Details",
modifier = Modifier.clickable {
navController.navigate(Screen.Details.route)
}
)
}
}
DetailScreen
@Composable
fun DetailsScreen(navController: NavHostController) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Details Screen - Click to go back",
modifier = Modifier.clickable {
navController.popBackStack()
}
)
}
}
NavigationGraph
@Composable
fun NavigationGraph(navController: NavHostController) {
NavHost(navController = navController, startDestination = Screen.Home.route) {
composable(Screen.Home.route) { HomeScreen(navController) }
composable(Screen.Details.route) { DetailsScreen(navController) }
}
}
In the above code we used a sealed class, In Jetpack Compose, using navigation arguments or a sealed class can help pass data between composable destinations.
But we are not passing any data or arguments in the above example but you can do it.