How to create a ViewModel for an Activity/Fragment

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 –

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

Leave a Reply

Your email address will not be published.