Working with ViewModels in Jetpack Compose

Until a few years ago, Google did not recommend a specific approach to building Android apps other than to provide tools and development kits while letting developers decide what worked best for a particular project or individual programming style. That changed in 2017 with the introduction of the Android Architecture Components which became part of Android Jetpack when it was released in 2018. Jetpack has of course, since been expanded with the addition of Compose.

This chapter will provide an overview of the concepts of Jetpack, Android app architecture recommendations, and the ViewModel component.

What is Android Jetpack?

Android Jetpack consists of Android Studio, the Android Architecture Components, Android Support Library, and the Compose framework together with a set of guidelines that recommend how an Android App should be structured. The Android Architecture Components were designed to make it quicker and easier both to perform common tasks when developing Android apps while also conforming to the key principle of the architectural guidelines. While many of these components have been superseded by features built into Compose, the ViewModel architecture component remains relevant today. Before exploring the ViewModel component, it first helps to understand both the old and new approaches to Android app architecture.

The “old” architecture

In the chapter entitled An Example Jetpack Compose Project, an Android project was created consisting of a single activity that contained all of the code for presenting and managing the user interface together with the back-end logic of the app. Up until the introduction of Jetpack, the most common architecture followed this paradigm with apps consisting of multiple activities (one for each screen within the app) with each activity class to some degree mixing user interface and back-end code.

This approach led to a range of problems related to the lifecycle of an app (for example an activity is destroyed and recreated each time the user rotates the device leading to the loss of any app data that had not been saved to some form of persistent storage) as well as issues such as inefficient navigation involving launching a new activity for each app screen accessed by the user.

 

You are reading a sample chapter from Jetpack Compose 1.6 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

Modern Android architecture

At the most basic level, Google now advocates single activity apps where different screens are loaded as content within the same activity.

Modern architecture guidelines also recommend separating different areas of responsibility within an app into entirely separate modules (a concept Google refers to as “separation of concerns”). One of the keys to this approach is the ViewModel component.

The ViewModel component

The purpose of ViewModel is to separate the user interface-related data model and logic of an app from the code responsible for displaying and managing the user interface and interacting with the operating system.

When designed in this way, an app will consist of one or more UI Controllers, such as an activity, together with ViewModel instances responsible for handling the data needed by those controllers.

A ViewModel is implemented as a separate class and contains state values containing the model data and functions that can be called to manage that data. The activity containing the user interface observes the model state values such that any value changes trigger a recomposition. User interface events relating to the model data such as a button click are configured to call the appropriate function within the ViewModel. This is, in fact, a direct implementation of the unidirectional data flow concept described in the chapter entitled Jetpack Compose State and Recomposition. The diagram in Figure 39-1 illustrates this concept as it relates to activities and ViewModels:

 

You are reading a sample chapter from Jetpack Compose 1.6 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

Figure 39-1

This separation of responsibility addresses the issues relating to the lifecycle of activities. Regardless of how many times an activity is recreated during the lifecycle of an app, the ViewModel instances remain in memory thereby maintaining data consistency. A ViewModel used by an activity, for example, will remain in memory until the activity finishes which, in the single activity app, is not until the app exits.

In addition to using ViewModels, the code responsible for gathering data from data sources such as web services or databases should be built into a separate repository module instead of being bundled with the view model. This topic will be covered in detail beginning with the chapter entitled Room Databases and Jetpack Compose.

ViewModel implementation using state

The main purpose of a ViewModel is to store data that can be observed by the user interface of an activity. This allows the user interface to react when changes occur to the ViewModel data. There are two ways to declare the data within a ViewModel so that it is observable. One option is to use the Compose state mechanism which has been used extensively throughout this book. An alternative approach is to use the Jetpack LiveData component, a topic that will be covered later in this chapter.

Much like the state declared within composables, ViewModel state is declared using the mutableStateOf group of functions. The following ViewModel declaration, for example, declares a state containing an integer count value with an initial value of 0:

 

You are reading a sample chapter from Jetpack Compose 1.6 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

class MyViewModel : ViewModel() {
 
    var customerCount by mutableStateOf(0)
}Code language: Kotlin (kotlin)

With some data encapsulated in the model, the next step is to add a function that can be called from within the UI to change the counter value:

class MyViewModel : ViewModel() {
 
    var customerCount by mutableStateOf(0)
 
    fun increaseCount() {
        customerCount++
    }
}Code language: Kotlin (kotlin)

Even complex models are nothing more than a continuation of these two basic state and function building blocks.

Connecting a ViewModel state to an activity

A ViewModel is of little use unless it can be used within the composables that make up the app user interface. All this requires is to pass an instance of the ViewModel as a parameter to a composable from which the state values and functions can be accessed. Programming convention recommends that these steps be performed in a composable dedicated solely for this task and located at the top of the screen’s composable hierarchy. The model state and event handler functions can then be passed to child composables as necessary. The following code shows an example of how a ViewModel might be accessed from within an activity:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ViewModelWorkTheme {
                Surface(color = MaterialTheme.colors.background) {
                    TopLevel()
                }
            }
        }
    }
}
 
@Composable
fun TopLevel(model: MyViewModel = viewModel()) {
    MainScreen(model.customerCount) { model.increaseCount() }
}
 
@Composable
fun MainScreen(count: Int, addCount: () -> Unit = {}) {
    Column(horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxWidth()) {
        Text("Total customers = $count",
        Modifier.padding(10.dp))
        Button(
            onClick = addCount,
        ) {
            Text(text = "Add a Customer")
        }
    }
}Code language: Kotlin (kotlin)

In the above example, the first function call is made by the onCreate() method to the TopLevel composable which is declared with a default ViewModel parameter initialized via a call to the viewModel() function:

@Composable
fun TopLevel(model: MyViewModel = viewModel()) {
.
.Code language: Kotlin (kotlin)

The viewModel() function is provided by the Compose view model lifecycle library which needs to be added to the build dependencies of a project when working with view models as follows:

 

You are reading a sample chapter from Jetpack Compose 1.6 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

dependencies {
.
.
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1'
.
.Code language: Gradle (gradle)

With access to the ViewModel instance, the TopLevel function is then able to obtain references to the view model customerCount state variable and increaseCount() function which it passes to the MainScreen composable:

MainScreen(model.customerCount) { model.increaseCount() }Code language: Kotlin (kotlin)

As implemented, Button clicks will result in calls to the view model increaseCount() function which, in turn, increments the customerCount state. This change in state triggers a recomposition of the user interface, resulting in the new customer count value appearing in the Text composable. The use of state and view models will be demonstrated in the chapter entitled A Jetpack Compose ViewModel Tutorial.

ViewModel implementation using LiveData

The Jetpack LiveData component predates the introduction of Compose and can be used as a wrapper around data values within a view model. Once contained in a LiveData instance, those variables become observable to composables within an activity. LiveData instances can be declared as being mutable using the MutableLiveData class, allowing the ViewModel functions to make changes to the underlying data value. An example view model designed to store a customer name could, for example, be implemented as follows using MutableLiveData instead of state:

class MyViewModel : ViewModel() {
 
    var customerName: MutableLiveData<String> = MutableLiveData("")
 
    fun setName(name: String) {
        customerName.value = name
    }
}Code language: Kotlin (kotlin)

Note that new values must be assigned to the live data variable via the value property.

Observing ViewModel LiveData within an activity

As with state, the first step when working with LiveData is to obtain an instance of the view model within an initialization composable:

 

You are reading a sample chapter from Jetpack Compose 1.6 Essentials.

Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

@Composable
fun TopLevel(model: MyViewModel = viewModel()) {
 
}Code language: Kotlin (kotlin)

Once we have access to a view model instance, the next step is to make the live data observable. This is achieved by calling the observeAsState() method on the live data object:

@Composable
fun TopLevel(model: MyViewModel = viewModel()) {
    var customerName: String by model.customerName.observeAsState("")
}Code language: Kotlin (kotlin)

In the above code, the observeAsState() call converts the live data value into a state instance and assigns it to the customerName variable. Once converted, the state will behave in the same way as any other state object, including triggering recompositions whenever the underlying value changes.

The use of LiveData and view models will be demonstrated in the chapter entitled A Jetpack Compose Room Database and Repository Tutorial.

Summary

Until recently, Google has tended not to recommend any particular approach to structuring an Android app. That changed with the introduction of Android Jetpack which consists of a set of tools, components, libraries, and architecture guidelines. These architectural guidelines recommend that an app project be divided into separate modules, each being responsible for a particular area of functionality, otherwise known as “separation of concerns”. In particular, the guidelines recommend separating the view data model of an app from the code responsible for handling the user interface. This is achieved using the ViewModel component. In this chapter, we have covered ViewModel-based architecture and demonstrated how this is implemented when developing with Compose. We have also explored how to observe and access view model data from within an activity using both state and LiveData.


Categories