Jetpack Compose has revolutionized Android UI development, offering a declarative and composable approach. However, as applications grow in complexity, managing navigation becomes crucial. Composable Destinations is a Kotlin compiler plugin designed to simplify navigation in Jetpack Compose applications. It automatically generates typesafe routes and arguments for your composable functions, making navigation more manageable and less error-prone.
What are Composable Destinations?
Composable Destinations is a navigation library specifically built for Jetpack Compose. It streamlines the process of defining navigation graphs by automatically generating routes and managing arguments. This ensures type safety and reduces boilerplate code.
Why Use Composable Destinations?
- Type Safety: Eliminates string-based route definitions, reducing potential runtime errors.
- Automatic Code Generation: Handles boilerplate code, allowing you to focus on UI development.
- Argument Handling: Simplifies passing and retrieving arguments to composables.
- Centralized Navigation: Manages navigation configurations in a single, organized place.
How to Implement Composable Destinations
To implement Composable Destinations in your Jetpack Compose project, follow these steps:
Step 1: Add Dependencies
Include the required dependencies in your build.gradle.kts file:
dependencies {
implementation("io.github.raamcosta.compose-destinations:core:1.9.55")
ksp("io.github.raamcosta.compose-destinations:ksp:1.9.55")
implementation("androidx.navigation:navigation-compose:2.7.0") // Optional, if you're using Navigation Compose directly
}
kapt {
correctErrorTypes = true
}
If you encounter errors, using correctErrorTypes = true
Apply the Kotlin Symbol Processing (KSP) plugin:
plugins {
id("com.google.devtools.ksp").version("1.9.21-1.0.16")
}
Step 2: Annotate Composable Destinations
Annotate your composable functions that serve as destinations with @Destination:
import com.ramcosta.composedestinations.annotation.Destination
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@Destination
@Composable
fun HomeScreen() {
Text(text = "Home Screen")
}
@Destination
@Composable
fun DetailsScreen(id: Int) {
Text(text = "Details Screen with ID: $id")
}
Step 3: Generate the Destinations Object
Rebuild your project. This will generate a `Destinations` object that contains references to all your annotated composables.
Step 4: Create a Navigation Host
Use the DestinationsNavHost to create a navigation host in your main composable:
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.ramcosta.composedestinations.DestinationsNavHost
@Composable
fun MainScreen() {
val navController: NavHostController = rememberNavController()
DestinationsNavHost(
navGraph = NavGraphs.root,
navController = navController
)
}
Step 5: Navigate Between Destinations
To navigate, use the generated Destinations object:
import androidx.compose.material.Button
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import androidx.compose.material.Text
@Destination
@Composable
fun HomeScreen(navigator: DestinationsNavigator) {
Column {
Text(text = "Home Screen")
Button(onClick = { navigator.navigate(DetailsScreenDestination(id = 123)) }) {
Text("Go to Details")
}
}
}
@Destination
@Composable
fun DetailsScreen(id: Int) {
Text(text = "Details Screen with ID: $id")
}
Here, DetailsScreenDestination(id = 123) creates a type-safe route to the DetailsScreen with the specified ID.
Step 6: Handle Arguments
Composable Destinations automatically handles arguments for your composables. Simply define the arguments in the composable function and use them:
import com.ramcosta.composedestinations.annotation.Destination
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@Destination
@Composable
fun ProfileScreen(userId: String) {
Text(text = "Profile Screen for User: $userId")
}
To navigate to the ProfileScreen:
Button(onClick = { navigator.navigate(ProfileScreenDestination(userId = "456")) }) {
Text("Go to Profile")
}
Advanced Features
Nested Navigation Graphs
For more complex applications, you can define nested navigation graphs to organize your destinations. First, define a navigation graph:
import com.ramcosta.composedestinations.annotation.NavGraph
import com.ramcosta.composedestinations.annotation.RootNavGraph
@RootNavGraph
@NavGraph
annotation class MainNavGraph(
val start: Boolean = false
)
@MainNavGraph(start = true)
@Destination
@Composable
fun Home(...) { ... }
@MainNavGraph
@Destination
@Composable
fun Profile(...) { ... }
Custom Arguments
You can also customize argument serialization and deserialization. By default, primitive types and strings are automatically supported.
Example: Building a Simple App with Multiple Screens
Let’s create a simple application with three screens: Home, Details, and Profile.
// Home Screen
@Destination(route = "home")
@Composable
fun HomeScreen(navigator: DestinationsNavigator) {
Column {
Text(text = "Home Screen")
Button(onClick = { navigator.navigate(DetailsScreenDestination(id = 123)) }) {
Text("Go to Details")
}
Button(onClick = { navigator.navigate(ProfileScreenDestination(userId = "456")) }) {
Text("Go to Profile")
}
}
}
// Details Screen
@Destination(route = "details")
@Composable
fun DetailsScreen(id: Int) {
Text(text = "Details Screen with ID: $id")
}
// Profile Screen
@Destination(route = "profile")
@Composable
fun ProfileScreen(userId: String) {
Text(text = "Profile Screen for User: $userId")
}
// Main App Composable
@Composable
fun MainScreen() {
val navController = rememberNavController()
DestinationsNavHost(
navGraph = NavGraphs.root,
navController = navController
)
}
Best Practices
- Modularize Navigation Graphs: Break down large navigation graphs into smaller, manageable modules.
- Use Type-Safe Navigation: Leverage Composable Destinations’ type safety to prevent errors.
- Handle Argument Passing Carefully: Ensure arguments are properly serialized and deserialized.
- Test Navigation Thoroughly: Write UI tests to verify navigation flows and argument handling.
Conclusion
Composable Destinations greatly simplifies navigation in Jetpack Compose by providing type safety, automatic code generation, and easy argument handling. By following these steps and best practices, you can create robust and maintainable navigation structures in your Compose applications, making navigation management efficient and streamlined.