Passing Data Between Screens: A Guide for Jetpack Compose Navigation

Teni Gada
5 min readMay 19, 2024

--

Jetpack Compose provides a powerful navigation system for building intuitive user experiences in your Android applications. While navigating between screens, you might need to share data objects from a list screen to a details screen. This blog post will explore two robust approaches to achieve this, along with their pros and cons, to guide you in choosing the best solution for your specific use case.

Challenges with Jetpack Compose Navigation Arguments

While Jetpack Compose navigation offers a convenient way to navigate between screens, it currently has limitations when it comes to passing complex data objects as arguments. Below are some key points:

Limitations of Jetpack Compose Navigation Arguments:

  • Jetpack Compose navigation arguments are currently limited to primitive data types and Parcelable objects. This means you cannot directly pass complex objects or custom data classes as arguments.
  • Navigation arguments are meant for small pieces of data that need to be passed between screens. For larger data sets or complex objects, it’s recommended to use a Shared ViewModel or other state management solutions.

Approaches to Pass Data Between Screens:

1. Passing Data as JSON String (Workaround)

A common workaround for this limitation is to convert your data object (e.g., TV show model) into a JSON string using libraries like GSON or Moshi. Here’s a breakdown of the steps involved:

  1. Creating the Data Model: Define a data class representing your TVShow object. This class will hold properties like title, description, and ratings.
data class TVShow(val title: String, val description: String, val rating: Float)

2. Serializing to JSON: On the list screen, when a user taps on a TVShow item, convert the corresponding TVShow.

// get selectedTvShow object
val tvShowJson = Gson().toJson(selectedTvShow)

3. Navigation with Argument: Use the navigation library to navigate to the details screen. Pass the serialized JSON string as an argument.

NavHostController.navigate("details_screen?tvShowJson=$tvShowJson")

4. Deserializing on the Details Screen: In the details screen’s composable function, access the received arguments and deserialize the JSON string back into the original TV show object.

val tvShowJson = navController.currentBackStackEntry?.arguments?.getString("tvShowJson")
val tvShow = Gson().fromJson(tvShowJson, TVShow::class.java)

5. Using the Object: Now you have the TVShow object on the details screen. Use its properties to populate the UI elements and display the show’s details.

Putting All together :

//List Screen
@Composable
fun TvShowListScreen(navController: NavController) {
val tvShows: List<TvShow> = TvShowList.tvShows
LazyColumn(
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp)
) {
items(
items = tvShows
) {
TvShowListItem(tvShow = it, { selectedShow->

//navigation by Converting the TvShow object into JSON string
val tvShowJsonString = Gson().toJson(selectedShow)
navController.navigate(
"tvShow_detail_screen?tvShow=${tvShowJsonString}"
)
})
}
}
}



@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController, startDestination = "tvShow_list_screen") {
composable(route = "tvShow_list_screen") {
TvShowListScreen(navController = navController)
}
//navigation by Passing the object using NavBackStackEntry
composable(route = “tvShow_detail_screen”
) {
TvShowDetail( navController = navController )
}

//navigation by Converting the TvShow object into JSON string
composable(
route = "tvShow_detail_screen?tvShow={tvShow}",
arguments = listOf(
navArgument(
name = "tvShow"
) {
type = NavType.StringType
defaultValue = ""
},
)
) {

val tvShowJsonString = it.arguments?.getString("tvShow")
val tvShow = Gson().fromJson(tvShowJsonString, TvShow::class.java)
TvShowDetail(navController = navController,tvShow = tvShow)
}
}
}

Advantages:

  • Simple to implement, especially for projects with minimal dependencies.
  • Well-suited for scenarios where object complexity is low.

Disadvantages:

  • Introduces unnecessary data conversion overhead, which can impact performance for frequent data passing or complex objects.
  • Doesn’t leverage the benefits of Jetpack Compose’s composable system for data management.

2. Shared ViewModel (Recommended)

This approach promotes better separation of concerns and avoids unnecessary data conversion. Here’s how it works:

  1. Create Shared ViewModel: Define a ViewModel class that holds your data object (TV show model) and exposes methods for accessing and updating it.
class TVShowsViewModel(val application: Application) : ViewModel() {
private val _selectedTvShow = MutableLiveData<TVShow?>(null)
val selectedTvShow: LiveData<TVShow?> = _selectedTvShow.asLiveData()

fun updateSelectedTvShow(tvShow: TVShow) {
_selectedTvShow.value = tvShow
}
}

2. Access ViewModel in Both Screens: In both your TVShows List and Details screens, get an instance of the shared ViewModel.

3. Passing Data: In the TVShows List screen, update the shared ViewModel’s data object with the selected TV show.

LazyColumn {
items(tvShows) { tvShow ->
Text(
text = tvShow.title,
modifier = Modifier
.fillMaxWidth()
.clickable {
viewModel.updateSelectedTvShow(tvShow)
navController.navigate("tvShowDetail")
}
.padding(16.dp)
)
}

4. Observing Data: In the TVShows Details screen, use the viewModel composable to access the same instance of the ViewModel. Observe the selectedTvShow property.

val viewModel: TVShowsViewModel = viewmodel()
val selectedTvShow = viewModel.selectedTvShow.value
if (selectedTvShow != null) {
// Display TVShow details using selectedTvShow.
} else {
// Handle the case where no TVShow is selected (e.g., show a placeholder)
}

Advantages:

  • Maintains clean separation of concerns between UI and data logic.
  • Enhances data flow management within your application.
  • Avoids redundant data conversion, improving performance for complex objects or frequent updates.

Disadvantages:

  • Requires additional setup with ViewModels compared to the JSON string approach.

Data Passing Approach | Advantages | Disadvantages

Choosing the Right Approach

  • Use the JSON string approach for simpler scenarios where object complexity is low, and conversion overhead isn’t a major concern. This might be suitable for prototypes or quick implementations.
  • For complex data objects or scenarios with frequent data updates, consider the shared ViewModel approach to maintain a cleaner separation of concerns and avoid redundant data conversion. This is ideal for production-ready applications with a focus on maintainability and performance.

Conclusion

By understanding these approaches, you can effectively pass objects between screens in your Jetpack Compose applications, enhancing the overall user experience and data flow within your app. Choose the method that best aligns with your project’s complexity and performance requirements. Remember to stay updated on the evolving Jetpack Compose navigation system for potential future improvements in object passing capabilities.

You can find the complete code for this example in this GitHub repository: Jetpack Compose Demo App on GitHub.

I hope this breakdown of data passing in Jetpack Compose navigation was valuable for your Android development journey. If so, please show your appreciation by giving this article a clap! It helps motivate me to create more content like this.

Feel free to leave comments or questions below. I’d love to hear your feedback and help you with any issues you encounter. Happy coding!!

Stay tuned for more tutorials and tips on modern Android development!!

--

--

Teni Gada

Android Developer | Sharing knowledge & learning from others