Jetpack Compose Navigation and Passing Data Between Screens

Introduction to Jetpack Compose Navigation

Navigating between screens and passing data efficiently are crucial aspects of any modern Android application. Jetpack Compose navigation and passing data provides a streamlined approach to handle screen transitions while maintaining state. Unlike the traditional fragment-based navigation, Jetpack Compose navigation offers a declarative API that integrates seamlessly with other Compose components. In this comprehensive guide, we'll explore how to implement Jetpack Compose navigation and passing data between screens in your Android applications.

The Compose Navigation component simplifies the process of moving between different composable destinations while passing data along the way. With Jetpack Compose navigation, you can create a single-activity application with multiple screens without the complexity of fragment transactions. Let's dive into the world of Jetpack Compose navigation and passing data to build more intuitive and responsive user interfaces.

Setting Up Jetpack Compose Navigation

Before implementing Jetpack Compose navigation and passing data, you need to add the necessary dependencies to your project. The Navigation Compose library is the foundation for handling navigation in Jetpack Compose applications.

Add the following dependency to your app's build.gradle file:

dependencies {  
    implementation("androidx.navigation:navigation-compose:2.7.5")  
}  

The Jetpack Compose Navigation component is built on top of the Navigation Architecture Component but is specifically designed for Compose. This dependency enables you to create a NavHost and define routes for navigation between composable screens.

Creating a NavHost for Jetpack Compose Navigation

The central piece of Jetpack Compose navigation is the NavHost composable. This component acts as a container for your navigation graph and manages the navigation between composable destinations.

Here's how to create a basic NavHost:

@Composable  
fun NavigationApp() {  
    val navController = rememberNavController()  
      
    NavHost(  
        navController = navController,  
        startDestination = "home"  
    ) {  
        composable("home") {  
            HomeScreen(navController)  
        }  
          
        composable("details") {  
            DetailsScreen(navController)  
        }  
    }  
}  

In this example, the NavHost defines two destinations for Jetpack Compose navigation: "home" and "details". The rememberNavController() function creates and remembers a NavController, which is the primary API for navigating between composable screens.

Navigating Between Screens in Jetpack Compose

Once you've set up the NavHost, navigating between screens in Jetpack Compose is straightforward using the NavController. The NavController provides methods like navigate() to perform screen navigation.

@Composable  
fun HomeScreen(navController: NavController) {  
    Column(  
        modifier = Modifier  
            .fillMaxSize()  
            .padding(16.dp),  
        verticalArrangement = Arrangement.Center,  
        horizontalAlignment = Alignment.CenterHorizontally  
    ) {  
        Text("Home Screen")  
          
        Button(  
            onClick = { navController.navigate("details") },  
            modifier = Modifier.padding(top = 16.dp)  
        ) {  
            Text("Navigate to Details")  
        }  
    }  
}  

In this example, clicking the button triggers navigation to the details screen using the NavController's navigate method. This is the core mechanism for Jetpack Compose navigation between screens.

Passing Data in Jetpack Compose Navigation

Passing data between screens is an essential part of most applications. Jetpack Compose navigation offers several approaches for passing data during navigation.

Method 1: Using Route Parameters for Passing Data

One way to implement data passing in Jetpack Compose navigation is through route parameters. This method is useful for passing simple data like IDs or strings.

NavHost(navController = navController, startDestination = "home") {  
    composable("home") {  
        HomeScreen(navController)  
    }  
      
    composable(  
        route = "details/{itemId}",  
        arguments = listOf(navArgument("itemId") { type = NavType.StringType })  
    ) { backStackEntry ->  
        val itemId = backStackEntry.arguments?.getString("itemId")  
        DetailsScreen(navController, itemId)  
    }  
}  

To navigate with parameters:

// In HomeScreen  
Button(  
    onClick = { navController.navigate("details/item123") }  
) {  
    Text("View Item Details")  
}  

This approach to passing data in Jetpack Compose navigation embeds the data directly in the route, making it accessible through the backStackEntry's arguments.

Method 2: Using NavArgs for Type-Safe Passing Data

For more complex data passing scenarios in Jetpack Compose navigation, you can use NavArgs with the Kotlin Parcelize feature for type safety:

First, define a Parcelable class:

@Parcelize  
data class ItemData(val id: String, val name: String, val description: String) : Parcelable  

Then, use it in your navigation graph:

NavHost(navController = navController, startDestination = "home") {  
    composable("home") {  
        HomeScreen(navController)  
    }  
      
    composable(  
        route = "details",  
        arguments = listOf(  
            navArgument("itemData") { type = NavType.ParcelableType(ItemData::class.java) }  
        )  
    ) { backStackEntry ->  
        val itemData = backStackEntry.arguments?.getParcelable<ItemData>("itemData")  
        DetailsScreen(navController, itemData)  
    }  
}  

To navigate with the Parcelable:

// In HomeScreen  
val itemData = ItemData("123", "Example Item", "This is an example item")  
Button(  
    onClick = {  
        navController.currentBackStackEntry?.arguments?.putParcelable("itemData", itemData)  
        navController.navigate("details")  
    }  
) {  
    Text("View Item Details")  
}  

This method provides type-safe data passing between screens in Jetpack Compose navigation.

Method 3: Using SavedStateHandle for Passing Data

Another approach to passing data in Jetpack Compose navigation is using SavedStateHandle, which preserves data across configuration changes:

// In ViewModel  
class DetailsViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {  
    val itemId: String? = savedStateHandle["itemId"]  
      
    // Use itemId to load data  
}  

To navigate with SavedStateHandle:

// In HomeScreen  
Button(  
    onClick = {  
        navController.currentBackStackEntry?.savedStateHandle?.set("itemId", "item123")  
        navController.navigate("details")  
    }  
) {  
    Text("View Item Details")  
}  

This method is particularly useful for preserving navigation data during configuration changes and process death.

Deep Linking with Jetpack Compose Navigation

Jetpack Compose navigation also supports deep linking, allowing you to navigate directly to a specific screen from outside your app. This feature enhances the user experience by providing shortcuts to relevant content.

NavHost(navController = navController, startDestination = "home") {  
    composable("home") {  
        HomeScreen(navController)  
    }  
      
    composable(  
        route = "details/{itemId}",  
        arguments = listOf(navArgument("itemId") { type = NavType.StringType }),  
        deepLinks = listOf(  
            navDeepLink {   
                uriPattern = "https://example.com/details/{itemId}"   
            }  
        )  
    ) { backStackEntry ->  
        val itemId = backStackEntry.arguments?.getString("itemId")  
        DetailsScreen(navController, itemId)  
    }  
}  

With this configuration, your app can respond to deep links like https://example.com/details/item123, navigating directly to the details screen and passing the item ID data.

Nested Navigation in Jetpack Compose

For more complex app structures, Jetpack Compose navigation supports nested navigation graphs. This feature allows you to organize your navigation structure hierarchically.

NavHost(navController = navController, startDestination = "home") {  
    composable("home") {  
        HomeScreen(navController)  
    }  
      
    navigation(startDestination = "profile/main", route = "profile") {  
        composable("profile/main") {  
            ProfileMainScreen(navController)  
        }  
          
        composable("profile/settings") {  
            ProfileSettingsScreen(navController)  
        }  
    }  
}  

This approach groups related screens together, making your navigation structure more organized and maintainable.

Complete Example of Jetpack Compose Navigation and Passing Data

Let's put everything together in a complete example that demonstrates Jetpack Compose navigation and passing data between screens:

import android.os.Bundle  
import android.os.Parcelable  
import androidx.activity.ComponentActivity  
import androidx.activity.compose.setContent  
import androidx.compose.foundation.layout.*  
import androidx.compose.material3.*  
import androidx.compose.runtime.*  
import androidx.compose.ui.Alignment  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.unit.dp  
import androidx.navigation.*  
import androidx.navigation.compose.*  
import kotlinx.parcelize.Parcelize  
  
@Parcelize  
data class ProductData(  
    val id: String,  
    val name: String,  
    val price: Double,  
    val description: String  
) : Parcelable  
  
class MainActivity : ComponentActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContent {  
            MaterialTheme {  
                Surface(  
                    modifier = Modifier.fillMaxSize(),  
                    color = MaterialTheme.colorScheme.background  
                ) {  
                    NavigationApp()  
                }  
            }  
        }  
    }  
}  
  
@Composable  
fun NavigationApp() {  
    val navController = rememberNavController()  
      
    NavHost(  
        navController = navController,  
        startDestination = "productList"  
    ) {  
        composable("productList") {  
            ProductListScreen(navController)  
        }  
          
        composable(  
            route = "productDetails/{productId}",  
            arguments = listOf(  
                navArgument("productId") { type = NavType.StringType }  
            )  
        ) { backStackEntry ->  
            val productId = backStackEntry.arguments?.getString("productId") ?: ""  
            val product = getProductById(productId)  // This would come from your repository  
              
            if (product != null) {  
                ProductDetailsScreen(navController, product)  
            } else {  
                ErrorScreen(navController, "Product not found")  
            }  
        }  
          
        composable(  
            route = "checkout/{productId}",  
            arguments = listOf(  
                navArgument("productId") { type = NavType.StringType }  
            )  
        ) { backStackEntry ->  
            val productId = backStackEntry.arguments?.getString("productId") ?: ""  
            val product = getProductById(productId)  
              
            if (product != null) {  
                CheckoutScreen(navController, product)  
            } else {  
                ErrorScreen(navController, "Product not found")  
            }  
        }  
    }  
}  
  
@Composable  
fun ProductListScreen(navController: NavController) {  
    val products = remember {  
        listOf(  
            ProductData("1", "Android Phone", 699.99, "Latest Android smartphone with advanced features"),  
            ProductData("2", "Bluetooth Headphones", 129.99, "Wireless headphones with noise cancellation"),  
            ProductData("3", "Smart Watch", 249.99, "Fitness tracker with heart rate monitoring")  
        )  
    }  
      
    Column(  
        modifier = Modifier  
            .fillMaxSize()  
            .padding(16.dp)  
    ) {  
        Text(  
            text = "Product Catalog",  
            style = MaterialTheme.typography.headlineMedium,  
            modifier = Modifier.padding(bottom = 16.dp)  
        )  
          
        products.forEach { product ->  
            Card(  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .padding(vertical = 8.dp)  
            ) {  
                Column(  
                    modifier = Modifier.padding(16.dp)  
                ) {  
                    Text(  
                        text = product.name,  
                        style = MaterialTheme.typography.titleLarge  
                    )  
                    Text(  
                        text = "$${product.price}",  
                        style = MaterialTheme.typography.bodyLarge,  
                        modifier = Modifier.padding(vertical = 4.dp)  
                    )  
                    Button(  
                        onClick = {   
                            navController.navigate("productDetails/${product.id}")  
                        },  
                        modifier = Modifier.padding(top = 8.dp)  
                    ) {  
                        Text("View Details")  
                    }  
                }  
            }  
        }  
    }  
}  
  
@Composable  
fun ProductDetailsScreen(navController: NavController, product: ProductData) {  
    Column(  
        modifier = Modifier  
            .fillMaxSize()  
            .padding(16.dp),  
        verticalArrangement = Arrangement.spacedBy(16.dp)  
    ) {  
        Text(  
            text = product.name,  
            style = MaterialTheme.typography.headlineMedium  
        )  
          
        Text(  
            text = "$${product.price}",  
            style = MaterialTheme.typography.titleLarge  
        )  
          
        Text(  
            text = product.description,  
            style = MaterialTheme.typography.bodyLarge  
        )  
          
        Row(  
            modifier = Modifier  
                .fillMaxWidth()  
                .padding(top = 16.dp),  
            horizontalArrangement = Arrangement.spacedBy(16.dp)  
        ) {  
            Button(  
                onClick = { navController.popBackStack() },  
                modifier = Modifier.weight(1f)  
            ) {  
                Text("Back")  
            }  
              
            Button(  
                onClick = { navController.navigate("checkout/${product.id}") },  
                modifier = Modifier.weight(1f)  
            ) {  
                Text("Buy Now")  
            }  
        }  
    }  
}  
  
@Composable  
fun CheckoutScreen(navController: NavController, product: ProductData) {  
    Column(  
        modifier = Modifier  
            .fillMaxSize()  
            .padding(16.dp),  
        verticalArrangement = Arrangement.spacedBy(16.dp)  
    ) {  
        Text(  
            text = "Checkout",  
            style = MaterialTheme.typography.headlineMedium  
        )  
          
        Card(  
            modifier = Modifier.fillMaxWidth()  
        ) {  
            Column(  
                modifier = Modifier.padding(16.dp)  
            ) {  
                Text(  
                    text = "Order Summary",  
                    style = MaterialTheme.typography.titleLarge,  
                    modifier = Modifier.padding(bottom = 8.dp)  
                )  
                  
                Row(  
                    modifier = Modifier.fillMaxWidth(),  
                    horizontalArrangement = Arrangement.SpaceBetween  
                ) {  
                    Text(text = "Product:", style = MaterialTheme.typography.bodyLarge)  
                    Text(text = product.name, style = MaterialTheme.typography.bodyLarge)  
                }  
                  
                Row(  
                    modifier = Modifier  
                        .fillMaxWidth()  
                        .padding(vertical = 8.dp),  
                    horizontalArrangement = Arrangement.SpaceBetween  
                ) {  
                    Text(text = "Price:", style = MaterialTheme.typography.bodyLarge)  
                    Text(text = "$${product.price}", style = MaterialTheme.typography.bodyLarge)  
                }  
                  
                Row(  
                    modifier = Modifier.fillMaxWidth(),  
                    horizontalArrangement = Arrangement.SpaceBetween  
                ) {  
                    Text(  
                        text = "Total:",   
                        style = MaterialTheme.typography.titleMedium  
                    )  
                    Text(  
                        text = "$${product.price}",   
                        style = MaterialTheme.typography.titleMedium  
                    )  
                }  
            }  
        }  
          
        Button(  
            onClick = { /* Process payment */ },  
            modifier = Modifier.fillMaxWidth()  
        ) {  
            Text("Complete Purchase")  
        }  
          
        OutlinedButton(  
            onClick = {   
                navController.navigate("productList") {  
                    popUpTo("productList") { inclusive = true }  
                }  
            },  
            modifier = Modifier.fillMaxWidth()  
        ) {  
            Text("Return to Product List")  
        }  
    }  
}  
  
@Composable  
fun ErrorScreen(navController: NavController, errorMessage: String) {  
    Column(  
        modifier = Modifier  
            .fillMaxSize()  
            .padding(16.dp),  
        verticalArrangement = Arrangement.Center,  
        horizontalAlignment = Alignment.CenterHorizontally  
    ) {  
        Text(  
            text = "Error",  
            style = MaterialTheme.typography.headlineMedium,  
            modifier = Modifier.padding(bottom = 8.dp)  
        )  
          
        Text(  
            text = errorMessage,  
            style = MaterialTheme.typography.bodyLarge,  
            modifier = Modifier.padding(bottom = 16.dp)  
        )  
          
        Button(  
            onClick = { navController.navigate("productList") {  
                popUpTo("productList") { inclusive = true }  
            } }  
        ) {  
            Text("Return to Product List")  
        }  
    }  
}  
  
// Helper function to simulate fetching a product  
fun getProductById(id: String): ProductData? {  
    val products = listOf(  
        ProductData("1", "Android Phone", 699.99, "Latest Android smartphone with advanced features"),  
        ProductData("2", "Bluetooth Headphones", 129.99, "Wireless headphones with noise cancellation"),  
        ProductData("3", "Smart Watch", 249.99, "Fitness tracker with heart rate monitoring")  
    )  
      
    return products.find { it.id == id }  
}  

In this comprehensive example, we've created a complete e-commerce app flow using Jetpack Compose navigation and passing data between screens. This showcases:

  1. A product list screen
  2. A product details screen that receives product data
  3. A checkout screen that also receives product data
  4. Error handling for invalid navigation

The example demonstrates various techniques for passing data in Jetpack Compose navigation, including route parameters and Parcelable objects.

Advanced Navigation Features in Jetpack Compose

Handling Back Navigation

Jetpack Compose navigation provides several ways to handle back navigation, ensuring a smooth user experience:

// Navigate back to previous screen  
Button(onClick = { navController.popBackStack() }) {  
    Text("Go Back")  
}  
  
// Navigate back to a specific destination  
Button(  
    onClick = {  
        navController.navigate("home") {  
            popUpTo("home") { inclusive = true }  
        }  
    }  
) {  
    Text("Back to Home")  
}  

The popUpTo option allows you to specify how much of the back stack to pop when navigating. This gives you fine-grained control over the navigation flow in your app.

Using NavigationResult for Returning Data

When you need to pass data back from a child screen to a parent screen, you can use the SavedStateHandle:

// In child screen  
Button(  
    onClick = {  
        navController.previousBackStackEntry?.savedStateHandle?.set("result", "Data from child")  
        navController.popBackStack()  
    }  
) {  
    Text("Return with Data")  
}  
  
// In parent screen  
val result = navController.currentBackStackEntry?.savedStateHandle?.get<String>("result")  
LaunchedEffect(result) {  
    result?.let {  
        // Use the returned data  
        println("Received result: $it")  
    }  
}  

This pattern enables two-way data passing in Jetpack Compose navigation.

Summary

Jetpack Compose navigation and passing data provide a powerful framework for building complex, multi-screen Android applications with minimal boilerplate code. By leveraging the declarative approach of Compose, you can create intuitive navigation flows while efficiently passing data between screens.

The key components we've covered include:

  1. Setting up the Navigation dependency
  2. Creating a NavHost and defining routes
  3. Navigating between screens using NavController
  4. Passing data between screens using route parameters, Parcelables, and SavedStateHandle
  5. Implementing deep links and nested navigation
  6. Handling back navigation and returning data

By mastering these concepts, you'll be able to create seamless navigation experiences in your Jetpack Compose applications while maintaining clean architecture and separation of concerns.