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 .
1 |
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 .
1 2 3 4 5 6 7 8 9 |
/***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 ),
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
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 –
1 2 3 |
viewModel= ViewModelProviders.of(this).get(AppsViewModel::class.java) |
For a better understanding let us break it up into the following two statements .
1 2 3 4 |
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 –
1 2 3 |
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 –
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/***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