In my previous blog on using Room with Android, I outlined how one can use Room to effectively store app data after a user has exited your app for a better user experience. If you haven’t read this blog, you can read it here:
But what about cases where your app is running and its state is changed—for example, when the phone’s orientation is changed from portrait to landscape and vice versa?
Typically, when you do this, the app restarts, thereby losing the app’s current state and starting afresh. While this flow is totally reasonable (as your app might have different resources for landscape and portrait mode), the underlying data it’s using is the same.
For example, in my AfterShoot app, I’m fetching and displaying the list of images, stored in a SQL database, from a user’s device. This list of images is fetched the first time the app is launched, and it needn’t be fetched from the db again since the db isn’t going to be updated.
When I rotate the app, this list is cleared and the images are fetched again from the database; which doesn’t make sense because we already fetched the images once from the database on the first launch.
To fix this issue, you might be prompted to use the handy onSaveInstanceState() method, and while it might work for most scenarios, the issue with this method is that the Bundle can only save a maximum of 1MB of data. In my case, the list of images I had was dangerously close to this limit (around 835 kb).
Using ViewModels is also great when you’re building an app that fetches and displays data from an API (for example a news app). Without ViewModels, you’re forced to fetch data from the API every time the phone is rotated.
Think of a scenario where you’re reading a piece of breaking news, but as soon as you rotate your phone to landscape mode, your app tells you to re-download that news! This would be incredibly frustrating—this kind of UX could make or break your app.
In this blog post, we’ll be looking at ViewModels and how they can help us in mitigating this issue.
Table of contents:
About ViewModels
ViewModels are designed to store and manage UI-related data in a lifecycle-aware way. By lifecycle-aware, we mean that the data will be stored only while your app is running. As soon as the app is stopped, the data will be destroyed.
This is great for us, as we only want to fetch the list of images from the database once, and no matter how many times the phone is rotated, we want that list to remain in memory.
As far as the lifecycle of a ViewModel goes, here’s what it looks like:
As you can see, the ViewModel scope stays alive throughout your app Activity/Fragment’s lifecycle and is only destroyed when the Activity/Fragment is destroyed. It also makes sure that the ViewModel isn’t destroyed if the Activity/Fragment is destroyed and then recreated due to orientation changes.
Using ViewModels in Android
Concerning AfterShoot, this is what my current code looks like:
class BadImageFragment : Fragment() {
// initialize the DAO
private val dao by lazy {
AfterShootDatabase.getDatabase(requireContext())?.getDao()!!
}
private lateinit var blurredImageList: List<Image>
private lateinit var overExposeImageList: List<Image>
private lateinit var underExposeImageList: List<Image>
private lateinit var blinkImageList: List<Image>
private lateinit var croppedImageList: List<Image>
// initialize all the lists. this is done everytime I change the phone's orientation
private suspend fun initLists() = withContext(Dispatchers.IO) {
blinkImageList = dao.getBlinkImages()
underExposeImageList = dao.getUnderExposedImages()
overExposeImageList = dao.getOverExposedImages()
blurredImageList = dao.getBlurredImages()
croppedImageList = dao.getCroppedFaceImages()
}
...
}
As you can see in the comments above, the lists are cleared and initialized every time my phone’s orientation is changed. While it might not look like much, the app’s performance and user experience can take a critical hit if this data plays a critical part in how the app works.
We’ll now replace this with ViewModels. Let’s look at the steps involved.
Step 1: Adding the ViewModel dependency
Before using a ViewModel, you need to add the following dependency to your app’s build.gradle file:
dependencies {
...
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
...
}
After this, we sync our project so that this dependency is downloaded and ready to be used.
Step 2: Define a ViewModel class that holds your variables
Once the dependency has been added, we need to define a class that will act as our ViewModel to contain the variables we need to be stored.
// if your variables don't need context, you can also extend from ViewModel instead
class ImagesViewModel(application: Application) : AndroidViewModel(application) {
}
Inside this class, we’ll now fetch and store all of our lists that we fetched and stored in the Fragment class earlier.
Step 3: Initialize your variables in the ViewModel
Now that we have created a ViewModel, the next step is to initialize all the variables that are to be stored here. Here’s what it looks like for me:
// if your variables don't need context, you can also extend from ViewModel instead
class ImagesViewModel(application: Application) : AndroidViewModel(application) {
// lazily initialize these variables, so that they're only initialized when needed
private val dao by lazy {
requireNotNull(AfterShootDatabase.getDatabase(application.applicationContext)?.getDao())
}
val blurredImageList: LiveData<List<Image>> by lazy {
dao.getBlurredImages()
}
val overExposeImageList: LiveData<List<Image>> by lazy {
dao.getOverExposedImages()
}
val underExposeImageList: LiveData<List<Image>> by lazy {
dao.getUnderExposedImages()
}
val blinkImageList: LiveData<List<Image>> by lazy {
dao.getBlinkImages()
}
val croppedImageList: LiveData<List<Image>> by lazy {
dao.getCroppedFaceImages()
}
val goodImageList: LiveData<List<Image>> by lazy {
dao.getGoodImages()
}
}
To get access to your context, you can use the handy application parameter passed into the ImageViewModel class.
Step 4: Accessing data from the ViewModel
In order to access the data from a Fragment or an Activity, we won’t be creating an instance of this class ourselves. Instead, we’ll let the ViewModelProviders class know which ViewModel we want to access, and it then takes care of either creating a new instance for us or returning an existing instance. This is what it looks like:
class BadImageFragment : Fragment() {
private lateinit var blurredImageList: LiveData<List<Image>>
private lateinit var overExposeImageList: LiveData<List<Image>>
private lateinit var underExposeImageList: LiveData<List<Image>>
private lateinit var blinkImageList: LiveData<List<Image>>
private lateinit var croppedImageList: LiveData<List<Image>>
private lateinit var goodImageList: LiveData<List<Image>>
// create an instance of the viewModel
private val imageModel by lazy {
// pass the LifeCycle owner as "this"; it can either be the current Fragment or Activity
ViewModelProviders.of(this).get(ImagesViewModel::class.java)
}
// once you have the viewmodel, simply access the variable you need
private suspend fun initLists() = withContext(Dispatchers.IO) {
blurredImageList = imageModel.blurredImageList
blinkImageList = imageModel.blinkImageList
underExposeImageList = imageModel.underExposeImageList
overExposeImageList = imageModel.overExposeImageList
croppedImageList = imageModel.croppedImageList
goodImageList = imageModel.goodImageList
}
...
}
As you can see, instead of creating a new instance of our ViewModel via its constructor, we passed the ViewModel we wanted and the LifeCycleOwner for it to the ViewModelProviders class, and it takes care of getting the ViewModel for us.
LifeCycleOwner here is the class that will govern the LifeCycle of our ViewModel. If you set the LifeCycleOwner as the current Activity, the ViewModel will live as long as the Activity lives. And if you set it as the current Fragment, then the ViewModel will live as long as your Fragment is alive.
And that’s it! Once you have your ViewModel, you can access data from it as usual and populate your app’s UI accordingly.
Word of caution with ViewModels
While using ViewModels, it’s important to note that you’re not storing any Android-specific components in your ViewModel. For example, using ViewModel to store an Activity or a View isn’t a good practice, as this might lead to memory leaks since the Activity or View whose reference is held by the ViewModel won’t be garbage collected as long as the ViewModel is alive.
ViewModels should ideally only be used to store non-Android-related data, which you need to fetch once during an Activity’s lifecycle.
If you want to look at my implementation of ViewModels in AfterShoot, you can find the source code for it on my GitHub repository:
Thanks for reading! If you enjoyed this story, please click the 👏 button and share it to help others find it! Feel free to leave a comment 💬 below.
Have feedback? Let’s connect on Twitter.
Comments 0 Responses