The problem that ViewModel solves

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 .

Leave a Reply

Your email address will not be published.