In this tutorial we are going to create a simple app to display the list of apps installed on a device . We’ll also have the option to filter the list to display only the apps installed by the user or only the system apps .
You may also like to take a look at the following links –
- Previous post : The problem that ViewModel solves .
- Complete source code : Code equivalent to this post can be found on the branch named – 2_solution_viewmodel_without_livedata .
We are going to learn how to use a ViewModel to assist the UI controller and solve the following problems (we have discussed the problems in more detail in the previous blog post) –
- Data loss on configuration change
- Wastage of resources
- Bloated UI components
Data loss on configuration change
An Activity or a Fragment gets killed and recreated on configuration change. So we lose the data we keep in such a UI controller or in a class whose instance becomes inaccessible after the associated UI controller is killed .
In our case the data we want to retain through a configuration change are the string filterKey and the list of apps named items . Moving these two variables to a ViewModel will solve our problem as ViewModel has the following properties –
- An instance of ViewModel survives configuration change .
- And the same instance of the ViewModel is provided to the new recreated instance of the Activity/Fragment .
Steps to create and use a ViewModel :
- Create a class that extends the class ViewModel or AndroidViewModel( if you need the application context in your ViewModel)
- In the Activity/Fragment get an instance of the ViewModel using a ViewModelProvider . A ViewModelProvider is responsible for retaining a ViewModel and providing it to the UI controller . If you instantiate a ViewModel yourself instead of using a ViewModelProvider you won’t get the above mentioned benefits that ViewModel provides .
- Communication between the UI controller and the ViewModel
Creating the ViewModel for our app : AppsViewModel
Let us create a class AppsViewModel and extend AndroidViewModel . We are extending AndroidViewModel instead of ViewModel as we need an instance of Context to get the package manager ; and we need a packagemanager instance to get the list of packages .
class AppsViewModel(application:Application): AndroidViewModel(application)
We want our AppsViewModel to hold two pieces of data –
- filterKey : the string to filter our list of apps . It can hold one of the values – all( default value, lists all apps) , system (filter the list to display only system apps) , downloaded ( filter the list to display the downloaded apps) . This value can be set from the AppFragment ( the fragment where we display the list, and the option to filter )
- items : this is ArrayList of PackageInfo . The method getList(filter: String) retrieves the list of apps (PackageInfo) and put in this list . We have to provide our AppFragment access to this list so that it can display the list of apps in a RecyclerView .
/***In AppsViewModel***/
private var items: ArrayList<PackageInfo> = ArrayList<PackageInfo>()
var filterKey="all"
set(value) {
field = value
updateList()
}
Whenever our filterKey gets a new value we call updateList() to clear the current list and get the new list as per the new filterKey using the getList(filter: String) method . Below is the basic code of our AppsViewModel (this is not complete as we don’t have a way to communicate with the UI controller yet ),
class AppsViewModel(application:Application): AndroidViewModel(application) {
private val context: Context = application.applicationContext
private var items: ArrayList<PackageInfo> = ArrayList<PackageInfo>()
var filterKey="all"
set(value) {
field = value
updateList()
}
fun start()
{
updateList()
}
fun updateList()
{
items.clear()
getList(filterKey)
}
fun getList(filter: String) {
when(filter)
{
"all"->items = context.packageManager?.getInstalledPackages(0) as ArrayList<PackageInfo>
"system"->{
for (pi in context.packageManager?.getInstalledPackages(0)!!)
{
if (pi.applicationInfo.flags and (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP or ApplicationInfo.FLAG_SYSTEM) > 0) {
items.add(pi)
}
}
}
"downloaded"->{
for (pi in context.packageManager?.getInstalledPackages(0)!!)
{
if (pi.applicationInfo.flags and (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP or ApplicationInfo.FLAG_SYSTEM) > 0) {
} else {
items.add(pi)
}
}
}
}
}
}
Create a ViewModel instance in Activity/Fragment
Now as we have moved all our data and the code manipulating those data to our AppsViewModel , we must get an instance of AppsViewModel in AppFragment to get the data for the UI .
And the right way to do this is with help of ViewModelProvider as shown below –
viewModel= ViewModelProviders.of(this).get(AppsViewModel::class.java)
For a better understanding let us break it up into the following two statements .
val viewModelProvider:ViewModelProvider=ViewModelProviders.of(this)
viewModel= viewModelProvider.get(AppsViewModel::class.java)
Here ViewModelProviders.of(this) takes the instance of the Activity or Fragment as the parameter, and creates an instance of ViewModelProvider . And this ViewModelProvider creates a ViewModel and retains it till the scope of the Activity or Fragment is alive .
And the wrong way of doing this is shown below –
viewmodel= AppsViewModel(applicationContext); //WRONG WAY,don't do this
This way it is going to create a new instance of the AppsViewModel every time the Activity/Fragment is recreated and thus we won’t get the benefits of using a ViewModel . After we get the complete flow of the app working , you can try this way and you’ll realize that the data gets lost on configuration change .
Communication between the UI controller and the ViewModel
Now comes the interesting part ; as our AppFragment is not responsible for creating the data it must interact with the AppsViewModel for it . Following are the moments when there is a need of communication between our AppFragment and AppsViewModel .
- The initial communication ( Fragment to ViewModel) : set up the initial list ; we can do this by calling viewModel.start() inside onResume() of AppFragment .
- Filtering option changes ( Fragment to ViewModel) : this is being done with viewModel.filterKey=filterKeyNew whenever the filter option is changed by the user .
- New list is ready (ViewModel to Fragment) : this one is tricky as we have to communicate from the ViewModel to the Fragment . Now you think that , we can easily create a callback interface , implement it in AppFragment and pass its instance from the Fragment to the ViewModel as shown below –
/***WRONG WAY***/
/***FOLLOWING CODE CAN CAUSE MEMORY LEAKS; USE LIVEDATA INSTEAD***/
/******AppsViewModel******/
interface AppsEventListener
{
fun onListChange(items:ArrayList<PackageInfo>)
}
private lateinit var listener:AppsEventListener
fun setListener(listener: AppsEventListener){
this.listener=listener
}
/*******AppFragment********/
viewModel.setListener(object : AppsViewModel.AppsEventListener{
override fun onListChange(items: ArrayList<PackageInfo>) {
listAdapter.setAppList(items)
}
})
But this is not the right way to communicate back from a ViewModel , because –
- with viewModel.setListener() we are passing an object that has a reference to the AppFragment instance .
- on configuration change our Activity and AppFragment will be destroyed and recreated
- but the same instance of AppsViewModel is alive and provided to the new instance of AppFragment
- now again with viewModel.setListener() the ViewModel instance gets a reference to the new instance of AppFragment
- and as we have not explicitly cleared the previous instance that was being referenced by the ViewModel it will remain unused somewhere in the memory , number of such instances will grow over time with a number of configuration changes ,thus causing a memory leak .
And thus –
A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
Solution : The recommended way is to use LiveData for communication between a UI controller and a ViewModel