ylliX - Online Advertising Network
The onAttach, onCreate, onDestroy, and onDetach methods in the Fragment lifecycle

Fragment Lifecycles in the Age of Jetpack


Fragments have… Complicated lifecycles, to say the least. Let’s take a look at these, and how they all fit into the world of Jetpack today.

The instance lifecycle

We’ll start from the middle, with the lifecycle of the Fragment class instance itself. I’ll, creatively, refer to this as the instance lifecycle.

This lifecycle technically begins when the Fragment instance is created, usually via a constructor call or a conventional newInstance method call. It may also be created by the Android system automatically in the process of restoring an application’s state. The instance lifecycle ends with the object being garbage collected after not being referenced anymore.

More practically speaking though, there are two pairs of lifecycle methods within this lifecycle that you’ll be using in your code. The onAttach and onDetach methods tell you when the Fragment is attached to a Context via FragmentManager, while the onCreate and onDestroy methods are where you’re generally supposed to initialize your Fragment and free up any resources used by it, respectively.


The onAttach, onCreate, onDestroy, and onDetach methods in the Fragment lifecycle

Since this is the lifecycle of the Fragment object in memory, any data you store in properties of the Fragment will only remain there for the scope of this lifecycle. There are multiple events which might cause your Fragment instance to be recreated, and lose such values, which is why the savedInstanceState mechanism offers state saving and restoration, via the onSaveInstanceState and onCreate methods.

Most famously, a Fragment instance is torn down and a new one is created when the application goes through a configuration change* – same as with Activities.

We often simplify configuration changes to orientation changes, i.e. rotating the screen, as that’s what developers tend to encounter first (and the most) when learning Android development. However, there are other events which also trigger the same kind of configuration change and consequent instance recreation, such as screen size or language changes, or dark mode being toggled.

Another event that will end this lifecycle is process death, when your application is removed from memory entirely due to memory constraints of the device.

*Barring the usage of the deprecated setRetainInstance method

The view lifecycle

Zooming in on the Fragment lifecycle described above, we’ll find a shorter lifecycle nested within the instance lifecycle. This is the view lifecycle, the lifecycle of the View instance that the Fragment displays UI on.

Within the broader instance lifecycle, a Fragment might have multiple views created and torn down, over and over again.


Multiple view lifecycles nested in a single instance lifecycle

For example, when you navigate away from a given Fragment, but it’s still in the backstack, its layout will be discarded, while the Fragment instance itself is kept in memory. When it needs to be displayed again (e.g. you navigate back to it), it will be prompted to inflate a new layout, and populate it with data again.

The view lifecycle starts with the onCreateView and onViewCreated calls (for inflation and initialization, respectively), and ends with onDestroyView.


The onCreateView, onViewCreated, and onDestroy methods nested within the broader instance lifecycle

Since the Fragment might live longer than its view, any references to View instances must be cleared in onDestroyView, as the layout that those Views are part of is no longer valid for the Fragment.

The logical lifecycle

Finally, zooming out from the lifecycles we’ve covered so far, we find the broadest lifecycle of a Fragment. I’ll refer to this as the logical lifecycle for lack of a better term. This lifecycle lasts the entire logical lifetime of the screen. It starts when the Fragment is first created, includes any configuration changes it goes through, and ends when the Fragment is destroyed for good, with no further recreations of it.

This is the lifecycle that the Jetpack ViewModel class lives in, and it’s meant to be the solution to the problem of Fragment recreation clearing data and cancelling operations started in the Fragment. When the Fragment is recreated, the ViewModel instance associated with it remains the same one as for the previous Fragment instance, which means that data placed in a ViewModel will survive these configuration changes, as will operations that are started within the ViewModel‘s scope.

In terms of methods in the code, the logical lifecycle starts when the first instance of the Fragment and the corresponding ViewModel instance is created (initialization can be placed in the ViewModel constructor), and ends with the onCleared method of the ViewModel, which is called just slightly before the onDestroy and onDetach calls of the very last Fragment instance.


Multiple instance lifecycles nested in a single logical lifecycle

Note that while this lifecycle will survive configuration changes, ViewModels are still stored in memory and are not persisted on disk, therefore they (and this lifecycle) will still not last through process death. (Perhaps a fourth lifecycle should also be named, which includes process restarts in addition to recreations within a process 🤔).

If you wish to persist data at the ViewModel level of your application, you can use the SavedStateHandle mechanism. I’ve written about this in An Early Look at ViewModel SavedState
and its follow-up, A Deep Dive into Extensible State Saving
.

Now that we’ve covered the lifecycles that belong to a Fragment, let’s see how we encounter them in practice, when using some of the latest Jetpack constructs.

LifecycleOwners and observing LiveData

Jetpack introduced the concept of a LifecycleOwner – a thing with a lifecycle. These can be used in various ways, one of the prominent ones being observing LiveData.

Initially, the support Fragment class itself was a LifecycleOwner, which owned the instance lifecycle of a Fragment (from onCreate to onDestroy). This is what you could use to set up observations, which meant passing this as a parameter to the observe method:

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

    viewModel.state.observe(this, Observer { ... })
}

The LiveData class has the ability of cleaning up registered observers itself when the associated lifecycle ends, which meant that registering an observer in onCreate with the Fragment itself as the lifecycle owner would have it automatically removed in onDestroy. Neat!

However, a problem arose when the values from a LiveData like this were used to update UI. Observers of a LiveData get the initial state when they start observing, and then they’re notified of changes when those happen. Having an observer set up in the instance lifecycle meant that when the view of the Fragment was recreated, the newly inflated layout wouldn’t be populated until the value of the LiveData changed, as otherwise there wasn’t a reason to call the connected observer again. In theory, it has already seen the latest value.


A newly created Fragment view not being populated with data until the next state update

The solution? Moving the observer to the onViewCreated method, so that a new one is set up for each view of the Fragment that’s created. The initial notification of the observer would make sure that the new UI is filled with data.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewModel.state.observe(this, Observer { ... })
}

However, if you did this while still using the Fragment as the LifecycleOwner, you’ve introduced a bug in the code. You are adding new observers at the start of the view lifecycle, which may run its course multiple times, but they’ll only be removed at the end of the instance lifecycle. They’ll be registered from onViewCreated until onDestroy, and as the Fragment receives new views, they’ll begin stacking up.

For example, when your Fragment is on its second view lifecycle after the view was destroyed and then inflated again, every value placed in the LiveData would trigger two observers, executing the same UI update code twice.


Duplicate state update problem happening due to incorrect cleanup of observers

Oddly, this was an ongoing problem for a bit after the initial release of the architecture components. You can find discussions and workarounds from back in 2017 in this GitHub issue.

One fairly popular one was a ViewLifecycleFragment implementation by Christophe Beyls, which extended Fragment, and added a new lifecycle within it, which starts in onViewCreated and ends in onDestroyView.

If you wanted to observe a LiveData correctly, you’d extend this class instead of Fragment, and use viewLifecycleOwner to set up the observer:

class ProfileFragment : ViewLifecycleFragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    
        viewModel.state.observe(viewLifecycleOwner, Observer { ... })
    }
}

The architecture components team eventually came around to fixing this shortcoming of the Fragment class, by introducing their own viewLifecycleOwner property with the same behaviour in the AndroidX Fragment class, eliminating the need for the workaround.

class ProfileFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    
        viewModel.state.observe(viewLifecycleOwner, Observer { ... })
    }
}


The correct observer setup that removes each one in onDestroyView

TL;DR: when observing LiveData to update the state of the UI, do it in onViewCreated, and use viewLifecycleOwner to set up the observer.

Coroutine integrations

Let’s talk coroutines now. If you’re using a ViewModel, then most of your background work such as fetching data from the network or disk should happen in the logical lifecycle. This makes sure that these data fetches continue through configuration changes, as it’s likely that you don’t want to cancel your requests and launch new ones when those happen (which you’d have to do if you performed these calls in the Fragment, in the instance lifecycle).

This means that you should use coroutines that are scoped to your ViewModel. An easy way to do this is by using the Jetpack viewModelScope extension:

class ProfileViewModel: ViewModel() {
    fun loadProfile() {
        viewModelScope.launch {
            // Suspending calls to fetch data
        }
    }
}

This scope is cancelled automatically when the onCleared method is called. If you inject or create your own scope in a ViewModel instead, remember to cancel it in the onCleared method manually.


Another exciting coroutine integration in Jetpack is lifecycleScope. This is an extension property on LifecycleOwner, and it will give you a scope that’s cancelled when the given owner’s lifecycle ends.

To start coroutines in the instance lifecycle of a Fragment, you can use lifecycleScope (which corresponds to this.lifecycleScope). Coroutines started in this scope will be cleaned up in onDestroy:

class ProfileFragment: Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        lifecycleScope.launch {
            // Suspending stuff
        }
    }
}

However, if you’re running coroutines that are UI related, such as using them for animations or collecting from a Flow for state updates similarly to observing LiveData, you’ll likely want to do this in the view lifecycle, for the reasons described in the previous section: starting them in the instance lifecycle would make them too long-lived, past the view they belong to.

To do this, you’d have to use viewLifecycleOwner.lifecycleScope when starting the coroutines. Since these will be cleared in onDestroyView, you should set them up in the corresponding method at the start of the lifecycle, which is onViewCreated:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewLifecycleOwner.lifecycleScope.launch {
        viewModel.stateFlow.collect { ... }            
    }
}

What’s next?

It’s clear that Fragment lifecycles are complex, and you have to keep the differences between them in mind whenever working with these APIs.

It’s been eluded to at last year’s Android Dev Summit that the instance and view lifecycles might end up being consolidated, so that only the view lifecycle remains, and when the view of a Fragment is torn down, the instance itself is destroyed with it. This is still yet to arrive, and would obviously be a breaking change in the Fragment APIs. Is it time for Fragment2?

If you want to learn more about using Jetpack things correctly, take a look at my code review of a Pokedex project which touches on a variety of Jetpack APIs.

Finally, some shoutouts. I wrote a bit about these topics before, but the idea for this proper article was first sparked by conversations with RayWenderlich.com team members (Evana Margáin and Nishant Srivastava), and then this tweet from Vasya Drobushkov made me actually sit down and write it all up.

In MVVM-like view architectures, view state isn’t enough to communicate from the ViewModel to the View. They have to be supplemented by some kind of events, which come with several challenges you should be aware of.

Handling the state of UI correctly is one of the prominent challenges of Android apps. Here are my subjective thoughts about some different approaches, which ones I tend to choose, and why.

StateFlow behaves as a state holder and a Flow of values at the same time. Due to conflation, a collector of a StateFlow might not receive all values that it holds over time. This article covers what that means for your tests.

RainbowCake is an Android architecture framework, providing tools and guidance for building modern Android applications.





Source link

Leave a Reply

Your email address will not be published. Required fields are marked *