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.

Edit (13 march 2020): has been officially deprecated and should be used instead. The code samples in this article have been updated accordingly.

The Architecture Components provide default implementations for activities and fragments. They allow you to store instances inside a to be reused across configuration changes. The usage with activities is quite straightforward because the activity lifecyle maps well to the 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 and are called again.

For this reason, the usually recommended place to initialize s and other asynchronous loading operations that will eventually interact with the view hierarchy is in . We can assume this is also the best place to initialize instances by subscribing a new . 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 onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, 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 ), 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 , 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 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

Your first attempt may be to simply move the observer subscription to instead of . But it won’t work as expected without adding more boilerplate code: 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 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 onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, 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 API to differentiate a result from no result for the current value, so it’s better not to return any 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 and also not forget to unsubscribe it in , 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 onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

vm.myData.observe(this, observer)
}

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

vm
.myData.removeObserver(observer)
}

One of the main benefits of using 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 , because it will eventually be unsubscribed automatically in . What’s important is that it’s unsubscribed before an identical one is subscribed in , 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 will deliver the latest result again automatically during , if any. The above function can be used like this:

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

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, 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 to be automatically unsubscribed in . 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 which provides an additional custom for the current view hierarchy. Here is an implementation in Java.

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

Note: won’t be called if returns , so the custom won’t be created for headless fragments and will also return 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 onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, 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 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 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 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 instance already used to contain the various .

With that architecture, the instances exposed by the will be automatically observed by the generated 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 and binds the ’s property directly to the field. When the is updated, the will immediately reflect its new value.

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

The 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 class, it will register itself as a new observer on the 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 is passed to the instance, the 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 .

Furthermore, the special observers used by the 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 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 using a ;
  • 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 in .
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.

--

--

Android developer from Belgium, blogging about advanced programming topics.

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
Christophe Beyls

Android developer from Belgium, blogging about advanced programming topics.