Leaking memory from ViewModel

In this post we are going to explore some of the problems that can be solved by using LiveData . 

A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context. 

A ViewModel lives longer than an Activity or a Fragment . An Activity or a Fragment gets destroyed and recreated on every configuration change , and the new instance gets the same instance of the ViewModel . Thus keeping a reference to Activity or Fragment in the ViewModel can lead to a memory leak and in the worst cast it can also cause a crash . Now let us try to experience these two scenarios .

Overview

We are going use the same codebase that we have been working on for the last few posts .

Our app displays the list of apps installed in the device . We have the following components –

  • AppFragement : this is responsible for displaying the list of apps to the user.
  • AppsViewModel : this is where we keep the data for the UI .

Lets leak some memory by keeping UI reference in ViewModel:

We shall use LeakCanary to detect memory leaks . So first of all let us add the dependency for LeakCanary in the app level build.gradle , the following line is enough for that .



    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'

Till now we have been retrieving the list of apps in the main thread , now let us create an AsyncTask to do this in the background . And we are going to use an Interface to communicate back to the our AppFragment .



class AppsViewModel(application:Application): AndroidViewModel(application) {

    interface AppsEventListener
    {
        fun onListChange(items:ArrayList<PackageInfo>)
    }
    private lateinit var lisener:AppsEventListener
    
    ...

    fun setListener(listener:AppsEventListener)
    {
        this.lisener=listener
    }

    ...
}

We are setting this listener from AppFragment as shown below :



viewModel.setListener(object : AppsViewModel.AppsEventListener{
            override fun onListChange(items: ArrayList<PackageInfo>) {

                listAdapter.setAppList(items)

            }
        })

Our GetAppsAsyncTask is shown below , we have added a delay of 10 seconds so that the background task takes a little longer to complete .




    class GetAppsAsyncTask(listener:AppsEventListener,filter: String,pm:PackageManager) : AsyncTask<Void, Void, ArrayList <PackageInfo>>() {
        var listener:AppsEventListener
        var filterStr:String
        var packageManager:PackageManager
        private var items: ArrayList<PackageInfo> =  ArrayList<PackageInfo>()

        init {
            this.listener=listener
            this.filterStr=filter
            this.packageManager=pm
        }

        override fun doInBackground(vararg params: Void?): ArrayList<PackageInfo> {

            /*Adding a delay of 10 seconds*/
            Thread.sleep(10000)
            when(filterStr)
            {
                "all"->items = packageManager?.getInstalledPackages(0) as ArrayList<PackageInfo>
                "system"->{
                    for (pi in 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 packageManager?.getInstalledPackages(0)!!)
                    {
                        if (pi.applicationInfo.flags and (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP or ApplicationInfo.FLAG_SYSTEM) > 0) {

                        } else {
                            items.add(pi)
                        }
                    }
                }
            }

            return items
        }

        override fun onPostExecute(result: ArrayList<PackageInfo>?) {
            super.onPostExecute(result)

            result?.let { this.listener.onListChange(it) }

        }
    }

Now if we rotate our device a few times we will notice that LeakCanary has detected some memory leaks .

Lets try crashing our app

Now if we try to access any component of the layout of our AppFragment after the long running task completes but before it completes if the devices has been rotated and the AppFragment has been destroyed and recreated ; we may get a crash .

To experience this let us add a TextView in the AppFragment‘s layout , and let us change its text inside onListChange() .


/**fragment_apps.xml**/
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true"
        android:orientation="vertical">

    <TextView
            android:id="@+id/text"
            android:padding="16dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
    />

    <androidx.recyclerview.widget.RecyclerView
            android:layout_below="@id/text"
            android:id="@+id/app_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
    />
</RelativeLayout>

/**AppFragment**/
 viewModel.setListener(object : AppsViewModel.AppsEventListener{
            override fun onListChange(items: ArrayList<PackageInfo>) {

                listAdapter.setAppList(items)

                text.setText("Showing "+viewModel.filterKey+" apps")
            }
        })

Now once if we rotate the device before our long running task completes we’ll get a crash .

Leave a Reply

Your email address will not be published.