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):
onActivityCreated()
has been officially deprecated andonViewCreated()
should be used instead. The code samples in this article have been updated accordingly.
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.
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 onViewCreated()
are called again.
For this reason, the usually recommended place to initialize Loader
s and other asynchronous loading operations that will eventually interact with the view hierarchy is in onViewCreated()
. 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 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 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 onViewCreated()
, 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 onViewCreated()
. 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 onViewCreated()
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 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 onViewCreated()
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 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 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 onViewCreated()
, 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 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 onViewCreated()
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 ifonCreateView()
returnsnull
, so the customLifecycleOwner
won’t be created for headless fragments andgetViewLifecycleOwner()
will also returnnull
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 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 theBinding
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 aFragmentPagerAdapter
; - 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()
inonCreateView()
.
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.