What is a view model and why should you use one in your Android app?

An Android application must successfully handle data in a wide range of scenarios. For example, when the device is rotated, the application's activities and fragments must restart to display the rotated content. In the process of restarting the activities and fragments, some data may be lost. For example, in an SMS messaging app, the user may have been drafting a message to their friend when the display rotates. If the drafted message is not handled appropriately, then the message could be lost and this would be frustrating for the user. To handle data during the runtime of the application, we can utilise a class called ViewModel. The ViewModel class contains tools and resources for storing and processing data for application components such as activities and fragments, even if the components need to be restarted. In this tutorial, we will explore the capabilities of the ViewModel class and discuss how building view models can help you handle runtime data more effectively.

What is a view model

The ViewModel class allows you to handle data on behalf of application components such as activities and fragments. View models are lifecycle-aware. Typically, the view model will be initialised when an application component is created, such as in the onCreate stage of the Android activity lifecycle and will persist until the component is removed. For example, a view model may be removed if the initialising activity runs a method called finish(), which is a method that runs when the activity is destroyed and will not restart. Likewise, a view model associated with a fragment will close when the fragment is detached from its parent activity. In this way, view models persist when the component is destroyed and restarted, providing the destruction is temporary. For example, configuration changes (such as switching the device from light theme to dark theme) and screen rotations require activity and fragment instances to be restarted. Even though the activity and fragment need to be recreated, the original view model instance persists, thereby preserving the data held by the view model. The new activity and fragment instances can then reconnect to the view model and restore the user's data.

View models offer additional features and capabilities compared to alternative data storage methods. For example, the saved instance state can preserve simple data such as strings and numbers, whereas view models can handle more complex data types such as Bitmap representations of images and custom data class objects. The data will be preserved until the initialising component is destroyed without the intention of restarting. In which case, a ViewModel lifecycle method called onCleared will shut down the view model. The ViewModel lifecycle also provides view models with a coroutine scope called viewModelScope, which allows view models to perform tasks asynchronously (behind the scenes) without disrupting other application processes. For example, you could use the viewModelScope to coordinate the retrieval of data from a database or online source. Any tasks associated with the viewModelScope are tied to the view model, so will be shut down automatically if the view model is closed and enters the onCleared stage of its lifecycle. If you're interested in learning more about coroutines, then see this tutorial.

viewmodel-activity-fragment-lifecycle.png

How to create a view model

To use a view model in your Android application, you must configure the application using a toolkit called Gradle. First, open the Module-level build.gradle file by navigating through Project > Gradle Scripts.

android-gradle-module.png

Next, refer to the dependencies element and add the following dependencies to import all the tools required to use a ViewModel in your application.

dependencies {
	// Existing dependencies
	
    def lifecycle_version = "2.4.1"
    def arch_version = "2.1.0"

    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
	implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version")
	testImplementation("androidx.arch.core:core-testing:$arch_version")
}

Don't forget to resync your project when prompted to do so!

resync-gradle.png

Once the necessary tools have been imported, we can create the view model. For example purposes, we will create a view model for a music app. The view model will help coordinate the play queue by storing a list of songs and the index of the currently playing song. First, create a new Kotlin class by right-clicking the folder in your project where you would like the view model to be stored then selecting New > Kotlin File/Class. If the view model will be primarily associated with an activity, then you should look to create the view model class in the same folder as the activity class, and likewise for fragments.

new-kotlin-class.png

Give the class a suitable name such as PlayQueueViewModel and select Class from the list of options.

play-queue-view-model.png

Once the PlayQueueViewModel.kt file opens in the editor, modify its code so it reads as follows:

class PlayQueueViewModel : ViewModel() {
    var playQueue: List<Song> = emptyList()
    var currentlyPlayingSongIndex = 0
}

In the above code, the PlayQueueViewModel class extends the ViewModel class, which means the PlayQueueViewModel class has access to all the tools and data required to behave as a view model. Inside the view model, a variable called playQueue is defined, which will store a list of Song objects (Song being a data class defined elsewhere in the application) that comprise the play queue, and a variable called currentlyPlayingSongIndex that holds an integer representing the index of the currently playing song within the play queue. Application components that initialise the PlayQueueViewModel will have access to both variables so they can view and update the play queue data as required.

The PlayQueueViewModel view model can then be initialised elsewhere in the application. For example, an activity could initialise the view model in its onCreate stage using the following code:

import androidx.activity.viewModels
				
class MainActivity : AppCompatActivity() {
    private val playQueueViewModel: PlayQueueViewModel by viewModels()
	
    override fun onCreate(savedInstanceState: Bundle?) {
		// Existing onCreate method code
	}
}

The PlayQueueViewModel instance held in the playQueueViewModel variable will persist until the MainActivity activity is destroyed without the intention of restarting.

Meanwhile, a fragment could initialise the view model using the following code:

import androidx.fragment.app.activityViewModels
				
class FirstFragment : Fragment() {
    private val playQueueViewModel: PlayQueueViewModel by activityViewModels()
}

If a fragment initialises a view model using the activityViewModels view model provider rather than viewModels, that means the view model instance is linked to the fragment's parent activity rather than the fragment. This is significant because it means the view model instance can be shared by multiple fragments, providing each fragment initialises the view model in the same way. If one fragment shuts down, the other fragments can continue to access the view model uninterrupted. Also, the fragments can use the view model to share data.

While your view model should not be able to access user interface components or their contexts (e.g. activities and fragments) directly, it is possible to link the view model with the broader application context. Application context may be required for access to system services or initialise data sources such as databases. To access the application context, you should edit your view model class to extend the AndroidViewModel class rather than the ViewModel class, as shown below:

class PlayQueueViewModel(application: Application) : AndroidViewModel(application) {

After making the above changes, you can access the application context elsewhere in the view model via the application parameter.

Importantly, view model classes built using the AndroidViewModel class need to be initialised slightly differently. You should define the view model in a lateinit variable, then initialise the variable in the appropriate lifecycle stage such as onCreate for activities and onCreateView for fragments.

class MainActivity : AppCompatActivity() {
    private lateinit var playQueueViewModel: PlayQueueViewModel
	
    override fun onCreate(savedInstanceState: Bundle?) {
		// Existing onCreate method code
		
		playQueueViewModel = ViewModelProvider(this)[PlayQueueViewModel::class.java]
	}
}

class FirstFragment : Fragment() {
    private lateinit var playQueueViewModel: PlayQueueViewModel
	
	override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
		// Existing onCreateView method code
		
		playQueueViewModel = ViewModelProvider(this)[PlayQueueViewModel::class.java]
	}
}

Accessing and updating the data in a view model

Interacting with the data in a view model is relatively straightforward. Continuing with our music player example, suppose we had a fragment called PlayQueueFragment. The PlayQueueFragment fragment could retrieve and update the play queue data stored in the view model using the code shown below:

import androidx.fragment.app.activityViewModels

class PlayQueueFragment : Fragment() {

    private val playQueueViewModel: PlayQueueViewModel by activityViewModels()
    private var playQueue: List<Song> = emptyList()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        playQueue = playQueueViewModel.playQueue
    }
    
    private fun changeCurrentlyPlayingSong(newIndex: Int) {
        playQueueViewModel.currentlyPlayingSongIndex = newIndex
    }
}

The onViewCreated method assigns the contents of the view model's playQueue variable to the fragment's playQueue variable. In doing so, the onViewCreated method ensures the fragment has access to the play queue as soon as the user interface becomes visible to the user. The above code also defines a method called changeCurrentlyPlayingSong, which accepts an integer value as an argument. The supplied integer represents the index of the currently playing song and is transferred to the view model's currentlyPlayingSongIndex variable. Altogether, the above code demonstrates how to retrieve and update view model data.

<<< Previous

Next >>>