Retaining Dagger components across configuration change using Service-Tree

If you’ve ever tried using Component dependencies (or subcomponents) using Dagger, you’ve probably run into the following problem: you create scoped dependencies for your Activity/Fragment — but when you rotate the screen, all your dependencies are recreated, because you create a new component.

If you by chance use the Ribot app template, their BaseActivity does the same thing:

public ActivityComponent activityComponent() {
if(activityComponent == null) {
activityComponent = DaggerActivityComponent.builder()
.activityModule(new ActivityModule(this))
.applicationComponent(
RibotApplication.get(this).getComponent())
.build();
}
return activityComponent;
}

If you actually want scoped dependencies though that *do* survive configuration change, then this won’t do!

But soon you realize that you’d need to store the components in some kind of global cache. But if each Activity and Fragment has its own component, then when you finish the Activity, how will you also clear the Fragments’ components? Such a nuisance!

Fortunately, if you create a hierarchical global cache, it’s much easier. And service-tree is a way to hierarchically store anything.

— — — — — — — — — — —

To show this on an example, I’ve created a Fragment-based one.

I specifically chose Fragments to prove yet again that willingly choosing to use Fragments is synonymous with masochism, and triples the amount of pain required to do something trivial. But that’s beside the point.

Creating the singleton component

First, we need a singleton component. This is typically created in CustomApplication class, because it is guaranteed to exist. Our ServiceTree is also a singleton, so it makes sense for our @Singleton module provider method to create it.

Then we can make this globally accessible:

Great! With that, we’ve created the global Service-Tree, and made the root component accessible to every node in the tree.

Creating the Activity component

Well, because Fragments are tricky, and after process death they get recreated by super.onCreate(savedInstanceState), we need to set up our Activity component before that call.

Creating the Fragment component

It should be no surprise that Fragments are the trickiest! Their lifecycle is the least predictable. Apparently, they get recreated by the AppCompatActivity’s super.onCreate(), therefore their recreation is out of our hands.

The only reliable callback to bind something to the Fragment is in onAttach(Context).

Funnily enough, this is of course called even when the Activity gets killed and recreated by the system (configuration change), but there is no difference between these events. The only way you’d be able to see that it’s a configuration change is by checking fragment.getView() == null.

Of course, we don’t need to worry about this now, because we can use serviceTree.hasNodeWithKey(); all we need is to create the services for our Fragments in onAttach(Context).

Considering we need to bind it to the parent (and the Activity will also have to handle the destruction of the Fragment nodes), we’ll do this in our Activity.

Destroying the singleton component

Technically, considering that the singleton component is bound to Application, and Application lasts as long as the process of the application exists; there is no need to destroy the singleton component. This is something the system can handle.

Destroying the Activity component

Thankfully, there is a reliable way of telling that an Activity is currently being killed for good. We can use onDestroy() callback with an isFinishing() check.

Destroying the Fragment component

Of course, I left the best for last :)

Apparently, if you use the Fragment backstack, then the Fragment lifecycle becomes remarkably strange. This should come off as no surprise at first glance, but it definitely still has its surprises.

If you just close the Activity, then serviceTree.removeNodeAndChildren() also removes the Fragments’ services from the tree.

However, if you navigate between fragments inside your Activity, and you set up your fragments as usual…

Then the only way you can keep track of your active fragments is by relying on getSupportFragmentManager().getFragments().

This is because if you do the typical navigation

Then your FirstFragment will get removed, and not detached.

Therefore you have no reliable way of telling if FirstFragment still exists or not, unless asking the FragmentManager for its active fragments, which still contains it.

Also, in order to support back presses, we override onBackPressed() as typically needed with fragments:

And now that we have proper forward and backward navigation.

Let’s make sure our services get destroyed on back navigation! For that, we need to register a FragmentManager.OnBackStackChangedListener, as that is the way we can be notified of that a fragment transaction has occurred.

FragmentManager.OnBackStackChangedListener gives us a method called onBackStackChanged(), but it does not give us any information regarding previous state or current new state, so we need to keep track of the previous state ourselves, and request the active fragments from the fragment manager to tell the new state.

Fun facts about the fragment manager:

  1. at start-up, the backstack listener is called, and the active fragments list is NULL
  2. on back navigation, the active fragments list STILL has size 2, and the second element is NULL

With that in mind, we can set it up like so:

With that, we can collect the “new active tags” from the non-null fragments from the non-null list (ha, ha), and can tell if the previous state we manually stored contained any elements that are no longer in the new state. We destroy those nodes, they are no longer needed.

— — — — — — — — — — — — —

Funnily enough, there’s actually still one more problem: the activeTags we’ve set up with the initial replace is NOT initialized on process death! Which means if you restart the app from SecondFragment and you navigate back, your activeTags is empty, and the new state will be FirstFragment, NULL. Meaning, the service for SecondFragment won’t get destroyed.

So what you CAN do is set up the activeTags AFTER super.onCreate(), in case it’s not yet initialized.

With that, we have a complete setup for Fragments that handles configuration change, process death, forward navigation and backward navigation.

Let’s see the whole thing again:

Truly, truly worthy of being a fragment-driven solution!

(Although to be fair, I could just store the active tag list in onSaveInstanceState(Bundle) instead of rebuilding it after super.onCreate())

— — — — — — — — —

Now if only:

  1. recreation of our application elements were predictable
  2. being notified of changes in our application state was reliable
  3. we didn’t have to explicitly store the previous state

Good news is, we could actually do that! We just have to ditch the fragment backstack. (And fragments, really.)

We can replace the BackstackChangeListener like this:

And then our Activity can look like this:

I don’t know about you, but this is more straightforward to me compared to:

  • checking active fragments, checking active fragment list for null, checking active fragment elements for null
  • adding a backstack listener that is called erratically
  • preserving previous state manually AND across process death as well

Conclusion

Apparently, storing a hierarchical chain of components is fairly easy using a hierarchical global storage.

And working with the Fragment backstack (and honestly, Fragments in general, because of how they’re remade by super.onCreate()) results in just as much extra effort as anyone would expect.

For the curious, the service-tree-fragment-example is available here.

Some more advanced usage of service-tree with simple-stack (creating nested stacks for viewgroups, or automatic state persistence and restoration for scoped services in the tree) is available here.

Show your support

Clapping shows how much you appreciated Zhuinden’s story.