A Subtle Memory Leak - Fragment, RecyclerView and its Adapter

Software Engineer
Search for a command to run...

Software Engineer
Nice article. What if you create the adapter locally inside onViewCreated? I think that will be good also
That's all good if you have static data and all of it is available when onViewCreated finishes. I guess that should work.
Introduction Last week, the Swift for Android working group announced that Swift now supports Android. This is an exciting milestone for developers who want to leverage Swift's powerful features in Android development. To help developers explore this...

In my previous article, I introduced the Swift Android Gradle Plugin that simplifies integrating Swift code into Android projects. To demonstrate the plugin in action, I built a sample Android app that generates fractal images using Swift and renders...

Taking Flight with Android XR: Building a Jetliner Demo I spent the last few days exploring Android XR, Google's latest platform for extended reality applications. After working through the documentation and experimenting with the SDK, I built a demo...

This article shows you how to eliminate the rebuild-test cycle when working with Android preferences. Pref Editor lets you modify values instantly.

The Iterator design pattern is a well-known and often-used pattern. It allows us to access elements in a collection without the need to know about the underlying storage details. We get this functionality from an object known as an iterator of the co...

Fragments are amazing: modular, reusable, own view layout, swappable, you name them. However, you need to be extra careful when it comes to dealing with their lifecycle. I mean the lifecycle diagram clearly depicts the number of callbacks they can respond to.

In particular, they kinda have two lifecycles: for the fragment itself and for the view it contains (not referring to headless fragments).
An Activity-view's lifecycle is closely tied to the Activity's lifecycle: the view is (commonly) inflated in onCreate. After onStop, if the Application's process is destroyed, the system destroys the activity (along with its view) and will invoke onCreate when resuming to allow you inflate your view again. Otherwise the view lives on till onDestroy.

In comparison, the fragment has a rather involved lifecycle: separating out specific view lifecycle callbacks. This was done to allow it achieve its intended functions. The framework asks us for a view in onCreateView and later destroys it in onDestroyView -- all the while a fragment instance could still be around.

In both cases, we should be extra cautious when holding on to view references after onStop has been called as this is a common source of leaks.
A typical use case of fragments is an app with a bottom navigation bar that swaps fragments in and out on the same activity. In one of the fragments, say Home, we can add a RecyclerView to display a listing of items. Here's a sample setup.

Source code:
Can you spot a potential leak from the code above? Hint: a leak occurs when we navigate to a different fragment, say the Dashboard.
Tip: I highly recommend you install LeakCanary in your application. It constantly monitors for retained objects (leaks) as you develop your app and they have a great explanation on how it works as well as how to find and fix a leak.
For this sample app, I had to set retainedVisibleThreshold = 1 in order to force a heap dump and analysis for any retained object that's detected.
LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 1)
When LeakCanary detects a retained object, it gives you a nice tree illustration of the references and possible suspects causing the leak. Here's a sample leak trace:

From the screenshot above, the squiggly lines denote possible references causing the leak. Starting from above, the fragment has a strong reference to the RecyclerView's adapter. The adapter owns an observable instance that in turn has a reference to an ArrayList of observers. The first observer in that array is our RecyclerView and lastly, we had set the RecyclerView to refer to the adapter. Convoluted? Here's an illustration.

The leak is as a result of retaining the RecyclerView after it has been detached. From the earlier discussion, the fragment's view is destroyed, through onDestroyView, when we transition to another fragment but the fragment itself is still in memory. As a consequence, the adapter instance retains the RecyclerView causing it to leak.
A fix for any retain cycle is to break the cycle.
We have two options to break the cycle in our fragment:
RecyclerViewfun onDestroyView() {
adapter = null // adapter is nullable
super.onDestroyView()
}
RecyclerView's adapter to nullfun onDestroyView() {
view?.findViewById<RecyclerView>(R.id.recycler_view)?.adapter = null
super.onDestroyView()
}

Subtle decisions made when working with frameworks can have great impact on your app. Understanding and using framework callbacks correctly can help mitigate common errors.
Employ tools and libraries such as the Android Studio profiler and LeakCanary to assist in uncovering hidden issues within your code. And lastly, embrace the framework to write great apps. Happy coding.