Let me start with the truth – “ViewModel is a choice , not a compulsion” .
You can use any architecture that works for you , and probably you have one ( if you don’t then please have one as bloating your UI components is the worst practice ) . And chances are , you have experienced all the different , well known issues related to android lifecycle events and the state of your app’s UI . So you may have found the solutions to all your problems and you are living happily now , but still give ViewModels a try as it is preferred by Google and you may realize that it solves the problems in a more elegant way .
In this tutorial we are going to create a simple app to display the list of apps installed on a device . And we’ll start with the problem that ViewModel solves .
The following code represents the main fragment of our app (responsible for displaying the list of apps) , we have not used any architecture . We have a method getList() get retrieve the list of installed apps and then we pass it to our AppListAdapter to display in a RecyclerView .
class AppFragment : Fragment() { private var items: ArrayList<PackageInfo> = ArrayList<PackageInfo>() private var filterKey="" private lateinit var listAdapter: AppListAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val view = inflater.inflate(R.layout.fragment_apps, container, false) setHasOptionsMenu(true) return view } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupListAdapter() updateList() } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.app_fr_menu, menu) } override fun onOptionsItemSelected(item: MenuItem?)= when(item?.itemId){ R.id.menu_filter-> { showFilteringPopUpMenu() true } else->false } private fun setupListAdapter() { listAdapter = AppListAdapter(items) app_list.layoutManager = LinearLayoutManager(activity) app_list.adapter = listAdapter } private fun showFilteringPopUpMenu() { PopupMenu(activity as AppCompatActivity, activity!!.findViewById<View>(R.id.menu_filter)).run { menuInflater.inflate(R.menu.filter_menu, menu) setOnMenuItemClickListener { val filterKeyNew = when (it.itemId) { R.id.all -> "all" R.id.system -> "system" R.id.downloaded -> "downloaded" else -> "all" } Toast.makeText(activity,filterKeyNew,Toast.LENGTH_LONG).show() updateList(filterKeyNew) true } show() } } fun updateList(filterKey:String="all") { if(filterKey!=this.filterKey) { items= ArrayList<PackageInfo>() getList(filterKey) listAdapter.setAppList(items) } } fun getList(filter: String) { when(filter) { "all"->items = activity?.packageManager?.getInstalledPackages(0) as ArrayList<PackageInfo> "system"->{ for (pi in activity?.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 activity?.packageManager?.getInstalledPackages(0)!!) { if (pi.applicationInfo.flags and (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP or ApplicationInfo.FLAG_SYSTEM) > 0) { } else { items.add(pi) } } } } } }
And our app looks like the one shown below , we also have a filter option to display only the system apps or the downloaded apps .
The problem : Our list gets reloaded on configuration change
Now let us experience the problem with data persistence on configuration change .
- Let us filter the list to display only the downloaded apps .
- Change the orientation of the device
- Now we no more have the filtered list of downloaded app , instead the default list of all apps gets loaded .
You can experiment with different filters and different types of configuration changes yourselves . You can know more about the different types of configuration changes here .
The video below demonstrates this behavior for some of the configuration changes .
The reason of such behavior is that our activity gets destroyed and recreated on configuration changes .
So for example if we have filtered the list and now –
filterKeyNew = “downloaded”
After configuration change –
filterKeyNew =”all”and
items: ArrayList<PackageInfo> = ArrayList<PackageInfo>()
Solving the problem with onSavedInstanceState : Yes we can save the filterKey in onSaveInstanceState and retrieve it in from the savedInstaceState bundle in onCreate . And then the getList() method with get the list of apps using the this filterKey .
Seems like our problem has been solved right? But imagine, instead of loading the list of apps installed in the device we are making some api requests to a remote server . And as we are only storing the filterKey , every time we rotate the screen ( or any other type of configuration change occurs) a new api request is made to the server which may keep the user waiting for the data . And this may lead to very bad user experience .
But can’t we save the complete filtered list of apps ( items in our case) in onSaveInstanceState() ? Well we can , but we shouldn’t ! as this may lead to a TransactionTooLargeException . onSaveInstanceState() is meant to store small amount of data ( only for primitive types and simple, small objects such as String ) .
Another problem : Violation of the principle “Separation of concerns”
Do we need an Activity to write the code to add two numbers ? Or do we need a Fragment to retrieve the list of apps ?
Definitely not . We need these UI components to interact with the users and the operation system . The code for the calculator or to retrieve list of apps can be written in any independent class .
And we don’t even have any control over these classes , they are managed by the OS and can get killed anytime .
How a ViewModel can help us here ?
Now as we know that what we are doing is a bad practice , we have to find a way to separate the UI and the business logic . There are many different approaches to handle this but we are concerned about ViewModels here .
An instance of a ViewModel can hold data in lifecycle conscious way . On configuration change an activity or fragment get destroyed and recreated . But ViewModels are retained and the same instance of the ViewModel is provided to the new instance of the Activity/Fragment .
Hence if we keep our filterKey (String) and the item (list of apps) in a ViewModel we won’t need to reload the list of apps again after configuration change .
And by moving getList(filter: String) from AppsFragment to a ViewModel we will relieve our AppsFragment from the additional tasks which it shouldn’t be held responsible for .