While fragments have greatly improved since they were famously advocated against, fragments still have some issues that make them far more difficult to deal with than most developers would like. In this piece, we will explore how fixing them would improve ease of use, and provide suggestions on how this can be done in a backwards-compatible way.
The Extra View Lifecycle
The first issue I’d like to explore is the fragment’s detached state. This is the state a fragment is put in if you use
fragmentManager.detach(), put the fragment in the back stack, or use in a
FragmentPagerAdapter. This creates an entirely new bespoke lifecycle for fragments that has to be considered. And unfortunately, if you don’t use one of the above features, you may not realize you have an issue until you add it later on.
I propose that instead, for the fragment back stack, the fragment would go through
onDestroy when it’s pushed onto the stack, and a new instance would be created when it’s popped back to. For example, instead of this:
You would get this.
Be sure to note, this is the same lifecycle you would get with a configuration change. Because of this, a correctly behaving app should still work correctly with this setup.
Here are some footguns we can avoid:
- Not clearing view references - Great news – you don’t have to worry about it now! Fragments live as long as their views, so they can simply be garbage-collected. There is no need to null them out in
- Trying to restore state in the wrong place - For some reason
onCreateView()takes a Bundle
savedInstanceState. This is very misleading because
onSaveInstanceState()may not have been called between destroying and recreating the views. With the new lifecycle, you don’t have to worry about it, since o
nSaveInstanceState()will have been called.
- Running into surprising behavior with Live Data that needs to be solved with a special lifecycle.
- And finally, what about
FragmentPagerAdapter? You can just use
The one trickier notion about this is how long a ViewModel should live. Right now, view models for fragments in the back stack do stick around. There are pros and cons for keeping them around, but I propose that they get destroyed when the fragment is put into the back stack. There’s potentially a long time before the view model’s view will show up again, and it may get destroyed anyway before that happens on process termination. Any important state should be put in the saved instance state or persisted to disk some other way.
So, how do we change this behavior without breaking existing apps? As mentioned before, we are just removing a potential lifecycle, not creating a new one. Because of this, I think we should rip the band-aid off and change the behavior in a version update. This really shouldn’t break well-behaving apps. There’s precedence for this with the loader behavior changes in 27.1.0. Alternatively, a more conservative route could be taken where a method is added to
FragmentManager to enable this backstack behavior. Something like
fragmentManager.setDestroyFragmentsInBackstack(true). While this would guarantee that existing code would not break, I worry that the much higher implementation complexity of supporting both ways will lead to other bugs and surprising edge-cases.
ViewPager, this appears to already be in the process of being solved with
ViewPager2. It currently only has a
FragmentStateAdapter implementation that replaces
FragmentStatePagerAdapter. As long as this remains the case we are good here.
FragmentTransaction.attach() would be deprecated.
By removing the fragment’s detached state we simplify greatly their lifecycle and make it easier to write bug-free code.
Is this fragment visible?
The second issue I’d like to explore is the difficulty telling if a fragment is visible to the user, particularly in
ViewPager. In most places, this is easy. If the fragment is showing it’s views, it is clearly visible to the user. However, with
ViewPager, off-screen fragments must be in the view hierarchy to allowing swiping and animating to take place. There are two approaches often taken to handle this, neither of them great. The first one is to notify fragments from the
PageChanged listener. The second one is to override
setUserVisibleHint. The issue is that neither of these methods are lifecycle-aware so it’s very hard to tell when they might be called in relation to other code in your fragment.
So what should we do instead? I propose using the existing
onPause/onResume lifecycle for this. The current page’s fragment would be resumed while the off-screen fragments would be paused. This would guarantee that the callbacks are called at expected times in the fragment lifecycle and would mean you could reuse the same concept between a fragment in a
ViewPager and somewhere else.
Since we are getting
ViewPager2 anyway, this would be a great time to make this behavior change. You’d be able to take advantage of the new behavior by switching to the new API.
By using the existing lifecycle callbacks, fragments don’t have to be specifically aware they are hosted in a
ViewPager, and detecting if one is showing are not becomes far easier.
Fragments have seen many improvements in usability since they were first released. However, with a growing movement towards single-activity apps, it becomes more-and-more important that fundamental issues with working with them are solved. I believe the above two changes would go a long way towards accomplishing that. If you would like to follow these issues or provide feedback to Google, please review the two issues below: