Patching Fragment memory leaks in 2019

Péter Pandula
Apr 18, 2019 · 6 min read

So you’re just starting a new project. Exciting times! This is going to be the one where everything goes smoothly. You (think you) have a stable and reliable architecture, you’re free to use any technologies you wish and you feel prepared to tackle whatever the design team has in store for you.

You spend some time setting up a nice environment for yourself: adding a few base classes, grabbing your favorite libraries, and all the rest of it. Maybe after a few days you already played around a bit with creating a simple navigation, there are a few screens for authentication, profile, settings, etc. and then it hits you: what about memory leaks? You’ve been around the block a few times, you’re not keeping static references to your Contexts, your observers are lifecycle-aware, but who knows, LeakCanary couldn’t hurt. So you might as well set it up, just in case.

Turns out that your simple app with around a dozen empty screens has serious memory management issues and maybe it’s time to re-think some of the core concepts you’ve taken for granted.

Image for post
Image for post
Credit: https://unsplash.com/@daanmooij

That’s how I’ve gotten myself into a week-long adventure, going through the issue tracker, debugging with the profiler, browsing through obscure StackOverflow threads and trying to make at least a simple app that doesn’t start leaking something after every single user interaction.

Architecture

The project contains a single Activity where Fragments represent the different screens. Sometimes there is some nesting going on, like in the case of the main screen which has a bottom navigation bar. I’m not yet using the Navigation architecture component, instead, I’m manually replacing the Fragments and handling the back stack based on tags. After a few iterations, this system seems to be functioning well. I’m working together with the framework instead of fighting it so state restoration is also fine.

The Fragments themselves use data binding and they are controlled by ViewModels from architecture components.

Now let’s see the problematic parts.

Data Binding

Update: Android Studio 3.4 stable has just been released!

The other problem is very similar, but in that case, I had no one else to blame: I was keeping a member property in the Fragments referencing their corresponding Binding class. The repro steps are the same: let’s say we open a modal screen — FragmentB — on top of FragmentA (replace FragmentTransaction + add to back stack), in which case the root view of FragmentA gets destroyed, but the class itself remains in memory. Keeping a reference to the binding makes garbage collecting the view hierarchy impossible. The solution is to make the binding reference nullable and mutable, resetting its value in onDestroyView(). I ended up using a similar pattern to getActivity() vs requireActivity() to get rid of redundant null checks: my base Fragment exposes a non-nullable binding (which has a nullable backing property: the actual binding reference) that throws an IllegalStateException when called outside of the normal view lifecycle.

AutoClearedValue

A convenient way for not having to override onDestroyView() is to use Kotlin’s property delegation. The Google samples contain a nice helper class called AutoClearedValue which wraps any type into a lifecycle-aware container that takes care of setting it to null when the time comes. The idea is great but we might be able to improve the implementation: for our purposes we need to observe the Fragment’s view lifecycle (not its “regular” one) and since that is not available the moment the Fragment gets instantiated, we should introduce some sort of lazy initialization (which makes asking for a constructor parameter redundant). Here is a version that seems to work fine so far:

We can declare auto-clearing properties like this:

var adapter by AutoClearedValue<SomeRecyclerAdapter>()

…and set their value just like we would for a simple property:

adapter = SomeRecyclerAdapter()

Delegation makes sure that when we initialize the property, behind the scenes our setter gets called.

Here is an improved version of my BaseFragment, using AutoClearedValue:

Transitions

I mentioned previously how I’m using single-Activity architecture for this project. The main advantage of this approach (not having to create a new Window for each screen) might be considered a bit of a disadvantage as well: by default, there are no transitions between the screens and Fragments get loaded fast — almost annoyingly fast. I wanted my detail screens to slide in, my modals to slide up and everything else to crossfade.

My go-to solution was the Transition API. Based on the type of the screen in the Fragment’s onCreate() I initialized the enterTransition, returnTransition, reenterTransition, and exitTransition fields. I wasn’t using anything custom: just simple Fade() and Slide() instances. LeakCanary went crazy. No problem, let’s reset these values to null after we don’t need them — same issue. I’ve tried the AndroidX implementation as well as the one from the SDK, but I couldn’t find a solution. Memory profiler, heap dumps, StackOverflow — been there, done that.

After two days I’ve decided to use a completely different API and the recently released Navigation component turned out to be a good source of inspiration. It’s using the setCustomAnimations() method of the FragmentTransaction — the one with four parameters covers all the transition types specified previously. For my use case, it was relatively easy to refactor the codebase and the end result looks good enough.

I still had some leaks when two transitions were running simultaneously in nested Fragments (the screen containing the bottom navigation bar was crossfading and the Fragment representing the currently selected menu item also had a fade animation). Disabling the animation the first time the screen is opened fixed the issue and also assured a smoother UX.

No more leaks

The general idea (which feels obvious, but apparently I wasn’t paying nearly enough attention to it) is that in Fragments any global property that can reference Views should be cleaned up when the root view gets destroyed. This is a no-brainer in case of Activities, where the content view doesn’t have its separate lifecycle, however, with the recent push towards single-Activity setups, we must be very aware of these concerns for Fragments.

Lastly, having LeakCanary in the project from the start should help manage the flow of unpleasant surprises. Third party libraries (and apparently even first-party ones) might introduce memory leaks and catching them as soon as possible gives us more time to find a fix.

____

Péter Pandula is an Android developer at Halcyon Mobile, a full-service mobile app design and development agency that creates award-winning mobile products for bold startups and brands.

The Halcyon Mobile Collection

A selection of mobile development, design and digital…

Péter Pandula

Written by

I’m an Android developer enthusiastic about new technologies with some background in game development.

The Halcyon Mobile Collection

A selection of mobile development, design and digital product related articles from the Halcyon Mobile team.

Péter Pandula

Written by

I’m an Android developer enthusiastic about new technologies with some background in game development.

The Halcyon Mobile Collection

A selection of mobile development, design and digital product related articles from the Halcyon Mobile team.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store