Skip to main content

Pass a ViewModel Across Multiple Pages in Jetpack Compose

Introduction

Jetpack Compose has revolutionized Android UI development by introducing a modern, declarative approach to building user interfaces. Unlike the traditional XML-based UI development, Jetpack Compose enables developers to create dynamic and efficient UIs with less boilerplate code. One of its strengths lies in seamlessly integrating with other Jetpack libraries, including ViewModel, Navigation, and LiveData.

In this blog post, we will focus on passing a ViewModel across multiple pages in Jetpack Compose. This is a common requirement in modern Android apps where shared state management and consistent UI behavior are critical. We’ll explore how Jetpack Compose simplifies this process while adhering to Android’s best practices. Whether you’re a seasoned Android developer or just starting out, this guide will provide practical insights to enhance your app development workflow.

Core Concepts Related to Jetpack Compose

Declarative UI

Jetpack Compose adopts a declarative paradigm, meaning developers describe what the UI should look like based on its state. Unlike imperative UI frameworks, Compose automatically updates the UI when the state changes, reducing complexity and potential bugs.

For example:

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

This snippet dynamically updates the text based on the name parameter.

State Management

Managing state is integral to building robust apps. Jetpack Compose offers tools like remember and rememberSaveable for local state and integrates seamlessly with ViewModel for shared state. ViewModels ensure state persistence across configuration changes, making them ideal for multi-page applications.

Navigation in Jetpack Compose

Jetpack Compose’s Navigation component simplifies navigation between screens. Using the NavHost and NavController, developers can define navigation graphs declaratively. This architecture is conducive to passing ViewModels between destinations, ensuring a consistent state.

Why Pass a ViewModel Across Multiple Pages?

Passing a ViewModel across pages is crucial for scenarios like:

  • User Authentication: Maintain user session state across login, profile, and dashboard pages.

  • E-commerce Apps: Share cart state across product listings, details, and checkout screens.

  • Media Applications: Persist playback state across multiple screens.

With Jetpack Compose, this process is streamlined thanks to its tight integration with Jetpack libraries.

Implementation Guide

Step 1: Setting Up the ViewModel

First, create a ViewModel to hold shared state. Use Jetpack’s ViewModel class to ensure lifecycle-aware state management.

class SharedViewModel : ViewModel() {
    private val _userName = MutableLiveData<String>()
    val userName: LiveData<String> = _userName

    fun updateUserName(newName: String) {
        _userName.value = newName
    }
}

Step 2: Setting Up Navigation

Define your navigation graph using the NavHost and NavController. Use dependency injection (e.g., Hilt) or NavGraphBuilder to provide the ViewModel across destinations.

@Composable
fun AppNavGraph(viewModel: SharedViewModel) {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(viewModel = viewModel, navController = navController)
        }
        composable("profile") {
            ProfileScreen(viewModel = viewModel)
        }
    }
}

Step 3: Using the ViewModel in Composable Functions

Inject the shared ViewModel into your composables to access or update the state.

Home Screen:

@Composable
fun HomeScreen(viewModel: SharedViewModel, navController: NavController) {
    val userName by viewModel.userName.observeAsState()

    Column {
        Text(text = "Welcome, ${userName ?: "Guest"}")
        Button(onClick = { navController.navigate("profile") }) {
            Text("Go to Profile")
        }
    }
}

Profile Screen:

@Composable
fun ProfileScreen(viewModel: SharedViewModel) {
    var newName by remember { mutableStateOf("") }

    Column {
        TextField(value = newName, onValueChange = { newName = it }, label = { Text("Enter new name") })
        Button(onClick = { viewModel.updateUserName(newName) }) {
            Text("Update Name")
        }
    }
}

Best Practices for ViewModel Sharing

  1. Use a Single Activity Architecture: Sharing a ViewModel is simpler in a single-activity app since all destinations are part of the same lifecycle scope.

  2. Leverage Hilt for Dependency Injection: Hilt provides a seamless way to inject ViewModels, ensuring lifecycle-aware and testable implementations.

  3. Minimize State Exposure: Avoid exposing mutable state outside the ViewModel. Use LiveData or StateFlow to encapsulate data.

  4. Optimize for Performance: Use recomposition-friendly patterns, such as derivedStateOf, to avoid unnecessary UI updates.

Advanced Features

Animations

Jetpack Compose provides intuitive APIs for animations. For instance, you can animate transitions between screens when navigating.

@Composable
fun AnimatedNavHost(navController: NavHostController) {
    NavHost(navController, startDestination = "home") {
        composable("home", enterTransition = { fadeIn() }, exitTransition = { fadeOut() }) {
            HomeScreen(...)
        }
        composable("profile", enterTransition = { slideInHorizontally() }) {
            ProfileScreen(...)
        }
    }
}

Combining Compose with XML

For projects transitioning to Jetpack Compose, integrate with existing XML layouts. For example, use ComposeView in an XML file to embed composables.

Real-World Use Case

Imagine building a task management app. You need to maintain task details across a list view, detail page, and editing screen. By using a shared ViewModel, you can:

  • Persist the task’s state across these pages.

  • Update the state from any screen and reflect changes instantly in others.

Here’s how you might structure the navigation:

NavHost(navController, startDestination = "taskList") {
    composable("taskList") { TaskListScreen(viewModel, navController) }
    composable("taskDetail/{taskId}") { backStackEntry ->
        val taskId = backStackEntry.arguments?.getString("taskId")
        TaskDetailScreen(viewModel, taskId)
    }
    composable("taskEdit") { TaskEditScreen(viewModel) }
}

Conclusion

Jetpack Compose is transforming Android UI development, and its integration with ViewModel simplifies state management across multiple pages. By following best practices and leveraging the tools provided by Jetpack, you can build efficient, maintainable, and user-friendly apps.

Start exploring these techniques in your next project and experience the power of Compose firsthand. Have questions or need guidance? Share your thoughts in the comments below or reach out to the Android developer community!

Popular posts from this blog

Restricting Jetpack Compose TextField to Numeric Input Only

Jetpack Compose has revolutionized Android development with its declarative approach, enabling developers to build modern, responsive UIs more efficiently. Among the many components provided by Compose, TextField is a critical building block for user input. However, ensuring that a TextField accepts only numeric input can pose challenges, especially when considering edge cases like empty fields, invalid characters, or localization nuances. In this blog post, we'll explore how to restrict a Jetpack Compose TextField to numeric input only, discussing both basic and advanced implementations. Why Restricting Input Matters Restricting user input to numeric values is a common requirement in apps dealing with forms, payment entries, age verifications, or any data where only numbers are valid. Properly validating input at the UI level enhances user experience, reduces backend validation overhead, and minimizes errors during data processing. Compose provides the flexibility to implement ...

jetpack compose - TextField remove underline

Compose TextField Remove Underline The TextField is the text input widget of android jetpack compose library. TextField is an equivalent widget of the android view system’s EditText widget. TextField is used to enter and modify text. The following jetpack compose tutorial will demonstrate to us how we can remove (actually hide) the underline from a TextField widget in an android application. We have to apply a simple trick to remove (hide) the underline from the TextField. The TextField constructor’s ‘colors’ argument allows us to set or change colors for TextField’s various components such as text color, cursor color, label color, error color, background color, focused and unfocused indicator color, etc. Jetpack developers can pass a TextFieldDefaults.textFieldColors() function with arguments value for the TextField ‘colors’ argument. There are many arguments for this ‘TextFieldDefaults.textFieldColors()’function such as textColor, disabledTextColor, backgroundColor, cursorC...

jetpack compose - Image clickable

Compose Image Clickable The Image widget allows android developers to display an image object to the app user interface using the jetpack compose library. Android app developers can show image objects to the Image widget from various sources such as painter resources, vector resources, bitmap, etc. Image is a very essential component of the jetpack compose library. Android app developers can change many properties of an Image widget by its modifiers such as size, shape, etc. We also can specify the Image object scaling algorithm, content description, etc. But how can we set a click event to an Image widget in a jetpack compose application? There is no built-in property/parameter/argument to set up an onClick event directly to the Image widget. This android application development tutorial will demonstrate to us how we can add a click event to the Image widget and make it clickable. Click event of a widget allow app users to execute a task such as showing a toast message by cli...