Compose Navigation: Passing Data Between Screens

In Jetpack Compose, navigation is an essential part of creating a multi-screen application. An important aspect of navigation is the ability to pass data between different composable screens. Passing data ensures that the state and context are maintained as users navigate through your app.

What is Compose Navigation?

Compose Navigation refers to the set of APIs provided by Android Jetpack to manage navigation within a Jetpack Compose application. It includes managing the back stack, handling transitions, and passing data between different composable destinations.

Why Pass Data Between Screens?

  • State Preservation: Maintaining state across screens ensures a consistent user experience.
  • Dynamic UI Updates: Data can drive changes in the UI based on the user’s interactions and selections.
  • Context Awareness: Knowing the context from previous screens allows for more personalized and relevant content.

Methods to Pass Data Between Screens

Jetpack Compose offers several ways to pass data between screens. Here, we’ll cover some of the most common approaches:

Method 1: Using Navigation Arguments

Navigation arguments allow you to define parameters directly in the route and pass data through the navigation call.

Step 1: Add Dependency

First, make sure you have the Compose Navigation dependency in your build.gradle file:

dependencies {
    implementation("androidx.navigation:navigation-compose:2.7.5") // or newer
}
Step 2: Define Navigation Graph and Routes

Create a navigation graph that defines the routes and their corresponding composables. Specify arguments in the route using curly braces {}.


import androidx.compose.runtime.Composable
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument

@Composable
fun NavigationGraph() {
    val navController = rememberNavController()
    
    NavHost(navController = navController, startDestination = "screenA") {
        composable("screenA") {
            ScreenA(navController = navController)
        }
        composable(
            "screenB/{data}",
            arguments = listOf(navArgument("data") { type = NavType.StringType })
        ) { backStackEntry ->
            val data = backStackEntry.arguments?.getString("data")
            ScreenB(navController = navController, data = data)
        }
    }
}
Step 3: Implement Screens and Data Passing

Implement the composable screens. ScreenA navigates to ScreenB and passes data. ScreenB receives this data from the backStackEntry.


import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavController

@Composable
fun ScreenA(navController: NavController) {
    Button(onClick = {
        navController.navigate("screenB/HelloCompose")
    }) {
        Text("Navigate to Screen B with Data")
    }
}

@Composable
fun ScreenB(navController: NavController, data: String?) {
    Text("Data from Screen A: ${data ?: "No data"}")
}

Method 2: Using NavController.currentBackStackEntry

You can pass data via the NavController‘s currentBackStackEntry, useful when direct navigation with arguments is less feasible.

Step 1: Store Data in currentBackStackEntry

Before navigating, save the data as a lifecycle-aware state in the currentBackStackEntry of the NavController of the current composable.


import androidx.compose.runtime.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.navigation.NavController

@Composable
fun ScreenC(navController: NavController) {
    var dataToPass by remember { mutableStateOf("ImportantData") }
    
    Button(onClick = {
        navController.currentBackStackEntry?.savedStateHandle?.set("data", dataToPass)
        navController.navigate("screenD")
    }) {
        Text("Navigate to Screen D with Data via SavedStateHandle")
    }
}
Step 2: Retrieve Data in the Destination Screen

In the destination composable, retrieve the data using the savedStateHandle.


import androidx.compose.runtime.*
import androidx.compose.material.Text
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState

@Composable
fun ScreenD(navController: NavController) {
    val navBackStackEntry = navController.currentBackStackEntryAsState()
    val data = navBackStackEntry.value?.savedStateHandle?.get("data")
    
    Text("Received data: ${data ?: "No data"}")
}
Step 3: Update Navigation Graph

Update the navigation graph to include these composables:


import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun NavigationGraphWithSavedState() {
    val navController = rememberNavController()
    
    NavHost(navController = navController, startDestination = "screenC") {
        composable("screenC") {
            ScreenC(navController = navController)
        }
        composable("screenD") {
            ScreenD(navController = navController)
        }
    }
}

Method 3: Using a Shared ViewModel

A ViewModel can be shared between different composables, providing a central repository for managing and sharing data.

Step 1: Create a Shared ViewModel

Create a ViewModel to hold the data that needs to be shared.


import androidx.lifecycle.ViewModel
import androidx.compose.runtime.*
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData

class SharedViewModel : ViewModel() {
    private val _sharedData = MutableLiveData()
    val sharedData: LiveData = _sharedData
    
    fun updateData(newData: String) {
        _sharedData.value = newData
    }
}
Step 2: Provide ViewModel to Composables

Use ViewModelProvider or viewModel() to access the shared ViewModel in your composables.


import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController

@Composable
fun ScreenE(navController: NavController, sharedViewModel: SharedViewModel = viewModel()) {
    var dataToShare by remember { mutableStateOf("Initial Data") }
    
    Button(onClick = {
        sharedViewModel.updateData(dataToShare)
        navController.navigate("screenF")
    }) {
        Text("Share Data and Navigate to Screen F")
    }
}

@Composable
fun ScreenF(navController: NavController, sharedViewModel: SharedViewModel = viewModel()) {
    val sharedData = sharedViewModel.sharedData.observeAsState()
    
    Text("Shared Data: ${sharedData.value ?: "No data"}")
}
Step 3: Update Navigation Graph

Update the navigation graph:


import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun NavigationGraphWithSharedViewModel() {
    val navController = rememberNavController()
    
    NavHost(navController = navController, startDestination = "screenE") {
        composable("screenE") {
            ScreenE(navController = navController)
        }
        composable("screenF") {
            ScreenF(navController = navController)
        }
    }
}

Conclusion

Passing data between screens is a fundamental requirement in most Android applications built with Jetpack Compose. By using navigation arguments, NavController‘s currentBackStackEntry, and shared ViewModel, you can efficiently manage and share data between different composable screens, ensuring a seamless and context-aware user experience. Choose the method that best fits your specific requirements and app architecture.