Architecture Components pitfalls — Part 1

LiveData and the Fragment lifecycle

The new Android Architecure Components are soon to be announced as stable after a few months of public testing.

A lot has already been written about the basics (starting with the very good documentation) so I won’t cover them here. Instead I would like to focus on important pitfalls that are mostly undocumented and rarely discussed and may cause issues in your applications if you miss them. In this first article, I’ll talk about our beloved Fragments.

Edit (14 may 2018): Google has finally fixed the issue in support library 28.0.0 and AndroidX 1.0.0. See solution 4 below.

The Architecture Components provide default ViewModelProvider implementations for activities and fragments. They allow you to store LiveData instances inside a ViewModel to be reused across configuration changes. The usage with activities is quite straightforward because the activity lifecyle maps well to the Lifecycle interface of the Architecture Components, but the fragment lifecycle is more complex and may cause subtle side effects if you’re not being careful.

The Fragment lifecycle (simplified version)

Fragments can be detached and re-attached. When they are detached, their view hierarchy is destroyed and they become invisible and inactive, but their instance is not destroyed. When they are later re-attached, a new view hierarchy is created and onCreateView() and onActivityCreated() are called again.

For this reason, the usually recommended place to initialize Loaders and other asynchronous loading operations that will eventually interact with the view hierarchy is in onActivityCreated(). We can assume this is also the best place to initialize LiveData instances by subscribing a new Observer. Most of the official Architecture Components samples also do it there. You would expect typical code to look like this:

class Page1Fragment : Fragment() {

private var textView: TextView? = null

override fun
onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_page1, container, false)
textView = view.findViewById(R.id.result)
return view
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val vm = ViewModelProviders.of(this)
.get(Page1ViewModel::class.java)

vm.myData.observe(this, Observer { textView?.text = it })
}

override fun onDestroyView() {
super.onDestroyView()
textView = null
}
}

There is a hidden problem with this code. We subscribe an anonymous observer within the lifespan of the fragment (using the fragment as LifecycleOwner), which means it will automatically be unsubscribed when the fragment is destroyed. However, when the fragment is detached and re-attached, it is not destroyed and a new identical observer instance will be added in onActivityCreated(), while the previous one has not been removed! The result is a growing number of identical observers being active at the same time and the same code being executed multiple times, which is both a memory leak and a performance problem (until the fragment is destroyed).

This problem concerns any fragment subscribing an observer for its own lifecycle in onCreateView() or later, without taking any other extra step to unsubscribe it.

Worse: it also impacts retained fragments, which are not destroyed during configuration changes but re-attached to a new Activity.

How do we solve this? There are a few solutions to explore, some better than others. Here are the ones I found so far.

1. Observing in onCreate()

Your first attempt may be to simply move the observer subscription to onCreate() instead of onActivityCreated(). But it won’t work as expected without adding more boilerplate code: LiveData keeps track of which result has been delivered to which observer, and it won’t deliver the latest result again to a new view hierarchy because the observer hasn’t changed. This means you have to manually check for a latest result and bind it to the new view hierarchy in onActivityCreated() which is not very elegant:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = ViewModelProviders.of(this)
.get(Page1ViewModel::class.java)

vm.myData.observe(this, Observer(this::bindResult))
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

vm.myData.value?.apply(this::bindResult)
}

private fun bindResult(result: String?) {
textView?.text = result
}

Note that the binding code needs to be called at two different places, so you can’t just write an inline anonymous observer. And finally, there is no way with the current LiveData API to differentiate a null result from no result for the current value, so it’s better not to return any null result (for instance in case of error) when using this solution, which overall is probably the worst one.

2. Manually unsubscribing the observer in onDestroyView()

This is a bit better than the first solution but you still can’t use an inline anonymous observer. You must declare it as a final field, subscribe it in onActivityCreated() and also not forget to unsubscribe it in onDestroyView(), so there is still boilerplate code and room for errors.

private val observer = Observer<String?> { textView?.text = it }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = ViewModelProviders.of(this)
.get(Page1ViewModel::class.java)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

vm.myData.observe(this, observer)
}

override fun onDestroyView() {
super.onDestroyView()
textView = null

vm
.myData.removeObserver(observer)
}

One of the main benefits of using LiveData being that it takes care of unsubscribing the observer for you, it’s a pity that it has to be done manually in this case. Keep reading, we can still do better.

3. Resetting an existing observer

It’s not actually required to unsubscribe the current observer precisely in onDestroyView(), because it will eventually be unsubscribed automatically in onDestroy(). What’s important is that it’s unsubscribed before an identical one is subscribed in onActivityCreated(), in order to avoid duplicates. Therefore, another valid solution is to unsubscribe right before subscribing, using for example this Kotlin extension function:

fun <T> LiveData<T>.reObserve(owner: LifecycleOwner, observer: Observer<T>) {
removeObserver(observer)
observe(owner, observer)
}

Removing and adding back the same observer will effectively reset its state so that LiveData will deliver the latest result again automatically during onStart(), if any. The above function can be used like this:

private val observer = Observer<String?> { textView?.text = it }

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val vm = ViewModelProviders.of(this)
.get(Page1ViewModel::class.java)

vm.myData.reObserve(this, observer)
}

The fragment code is now less error-prone because there is one less callback to add, but we still need to declare the observer instance as a final field and reuse it, or else unsubscription will silently fail. Despite this, it’s probably my personal favorite solution for a quick workaround.

4. Creating a custom Lifecyle for view hierarchies

Ideally, we would want observers subscribed in onActivityCreated() to be automatically unsubscribed in onDestroyView(). This means we actually want to follow the current view hierarchy lifecycle, which is different from the fragment lifecycle. One way to achieve this is to create a custom Fragment which provides an additional custom LifecycleOwner for the current view hierarchy. Here is an implementation in Java.

Thanks to the custom LifecycleOwner returned by getViewLifecycleOwner(), the LiveData observers will be automatically unsubscribed when the view hierarchy is destroyed and nothing else has to be done.

Note: onViewCreated() won’t be called if onCreateView() returns null, so the custom LifecycleOwner won’t be created for headless fragments and getViewLifecycleOwner() will also return null in that case.

With this solution the code is now nearly identical to the initial code sample, but this time it does the right thing.

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val vm = ViewModelProviders.of(this)
.get(Page1ViewModel::class.java)

vm.myData.observe(viewLifecycleOwner,
Observer { textView?.text = it })
}

The downside is that it requires inheriting from a custom Fragment implementation. It is also possible to implement the same functionality without inheritance but then it requires declaring a delegate helper class inside each fragment where that custom LifecycleOwner is needed.

Edit (14 may 2018): Google implemented this solution directly in support library 28.0.0 and AndroidX 1.0.0. All fragments now provide an additional getViewLifecycleOwner() method just like in the above sample, so you don’t need to implement it yourself.

5. Using Data Binding

This section has been rewritten following the release of Android Studio 3.1 and the described solution is considered production-ready.

This last solution provides the cleanest architecture, at the expense of depending on an additional library. It consists of using the Android Data Binding Library to automatically bind your model to the current view hierarchy, and to simplify things the model will be the ViewModel instance already used to contain the various LiveData.

With that architecture, the LiveData instances exposed by the ViewModel will be automatically observed by the generated Binding class instead of the fragment itself.

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
<variable
name="viewModel"
type="my.app.viewmodel.Page1ViewModel"
/>
</data>
    <TextView
android:id="@+id/result"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{viewModel.myData}"/>
</layout>

The layout declares a variable of type Page1ViewModel and binds the TextView’s android:text property directly to the LiveData field. When the LiveData is updated, the TextView will immediately reflect its new value.

Note: The ability to bind LiveData fields directly to views and make the Binding classes lifecycle-aware has been added in Android Studio 3.1.

The ViewModel will always reflect the latest visual state of the fragment, even when no view hierarchy is currently attached to it. Each time a view hierarchy is created through a Binding class, it will register itself as a new observer on the LiveData instances, so we don’t have to manage any observer manually anymore and we got rid of the problem.

class Page1Fragment : Fragment() {

lateinit var vm: Page1ViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = ViewModelProviders.of(this)
.get(Page1ViewModel::class.java)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentPage1Binding
.inflate(inflater, container, false)
binding.viewModel = vm
binding.setLifecycleOwner(this)
return binding.root
}
}

Since the LifecycleOwner is passed to the Binding instance, the LiveData loading logic will still comply with the lifecycle of the current fragment: the views will only be updated while the fragment is started and the internal observers will be properly unsubscribed in onDestroy().

Furthermore, the special LiveData observers used by the Binding classes use weak references internally so they will eventually be unregistered automatically after their associated view hierarchy has been destroyed and garbage collected, even if the fragment itself has not been destroyed yet.

Overall, this simplifies the fragment by removing the tricky observers logic along with all the View boilerplate code.


Final warning

This issue should be taken seriously because detaching a fragment is a very common operation. For instance, it happens when:

  • Switching between sections in the main activity of an app;
  • Navigating between pages in a ViewPager using a FragmentPagerAdapter;
  • A fragment is replaced with another one and the transaction is added to the back stack.

Also, when a fragment is detached, all its child fragments are detached as well. When in doubt, it’s better to assume that any fragment will eventually be detached at some point and properly handle this case from day 1.


Now you are aware of this behavior and have at least a few options to work around it.

Edit (14 may 2018): Google decided to implement solution 4 directly in support library 28.0.0 and AndroidX 1.0.0. I now recommend that you register your observers using the special lifecycle returned by getViewLifecycleOwner() in onCreateView().
I like to think that this article played a part in Google fixing this issue and picking up the proposed solution.

Don’t hesitate to discuss this in the comments section and please share if you like.