Sitemap

Surviving Configuration Changes using Dagger — DIY

--

Introduction

It’s been two years since I wrote about a clean Android architecture in this article. A year after that one, I leveraged it with a slightly different approach and making it lifecycle-aware in this other article. Now it’s time to close the circle and make it survive configuration changes.

Config-What?

Device configurations can change during runtime (screen rotation, language change, …). When that happens, the visible Activity is restarted and onDestroy() is called followed by onCreate(). Fragments suffer the same process.

After a configuration change, all the objects are going to be created again.

The old objects will be cannon fodder for the Garbage Collector and the new ones won’t keep the state the others have.

The goal is to reuse Objects between configuration changes to keep the states the objects are in and be more efficient

What options do I have?

Do it yourself:

1) Retain objects manually: Activity provides methods to save and restore the previous state of the app through its lifecycle. onRetainCustomNonConfigurationInstance() and getLastCustomNonConfigurationInstance()are methods available to do this.

2) Retain Instance in Fragments: Use setRetainInstance(true) in onCreate() to retain the Fragment across configuration changes. The activity will be still recreated though. Take into consideration that onDestroy() is not called on the Fragment but onAttach() and onActivityCreated(Bundle) are. Remember that this can only be used with Fragments that are not in the back stack. And also it doesn’t work on Nested fragments (neither on the parent fragment nor the rest of them).

3) Loaders: You can use them to asynchronously load data in an Activity or Fragment. Because Loaders survive configuration changes, you could set one up to achieve our goal.

Don’t do it yourself:

Note: that these libraries will only make your ViewModel object survive, not any other shared object that you might have in your Dagger Component.

DIY Solution

Our solution is based on Option 1: we’re going to retain the objects manually. Because we’re using Dagger, we’d have to retain the objects in the graph. Long story short…

We’re going to create a mechanism that is going to use the lifecycle methods to store a Cache of Components that won’t be recreated during configuration changes.

In this case, Dagger will provide the same objects that it provided before the configuration change.

Features

  • Consistency. One solution! we are not going to use Loaders here, setRetainInstance there…
  • Easy to integrate with your current code.
  • Supports MVx (MVVM, MVC…) and works with nested fragments and DI with Dagger 2.
  • Aware of the Android lifecycle.

Show me the code

You can take a look at the code in this repository.

Realize that…

  • We have a BaseComponent that injects objects to BaseActivity and BaseFragment.
  • Different features are going to use different Components.
  • Components are only created once.
  • We use onRetainCustomNonConfigurationInstance() and getLastCustomNonConfigurationInstance() on the Activity level to store the components that feature is using.

Let’s explain it!

What’s stored/retained across configuration changes?

Something we call the ComponentCache!

class ComponentCache {     
private val components = ArrayMap<Int, Any>(1)
...
}

Where Int is an unique identifier and the object is the Component itself.

Where is the “is the View finished?” logic?

How do we know if a View (Activity/Fragment) is “finished” by good or just temporarily? (due to a configuration change, low memory conditions, …)

We combine the methods onSaveInstanceState(Bundle) and onDestroy() to know if the Activity or Fragment is going to be recreated or completely destroyed

On a configuration change…

  • šonDestroy() is called. That doesn’t mean that it won’t be created again (we don’t have enough information right now).
  • šThe system will call onSaveInstanceState if it knows that the view will likely be re-created at some point (it’s not clear when it’s going to be called though).

You can see that logic in the ComponentLoader class (we’ll see it in more detail in another section).

class ComponentLoader<C> (...) {
private var isDestroyedBySystem: Boolean = false
fun onResume() {
isDestroyedBySystem = false
}
fun onSaveInstanceState() {
isDestroyedBySystem = true
}
fun onDestroy() {
if (!isDestroyedBySystem) {
// User is exiting this view, removeComponent component from the componentManager
componentCache.removeComponent(componentId)
}
}
}

If onSaveInstanceState has been called before onDestroy, it means that it’s likely it’s going through a configuration change

So we can remove the Component from the Cache since it’s not needed anymore.

Activity level architecture

UML Class Diagram of the Activity level architecture to survive configuration changes

We’re going to have a ComponentManager which is in charge of interacting with the ComponentCache. What’s the purpose of that ComponentLoader we see in the diagram? It’s going to allow us reuse the same Component multiple times. This is why we are going to call the ComponentLoader when we want to create a Component (we’ll see an example of this at the end of the article).

A ComponentLoader is of a certain type C. These loaders are going to be destroyed on a configuration change. So it might happen that the type of Component this ComponentLoader is in charge of is already in the ComponentCache. If that happens, we don’t have to create the component again, we just reuse it.

class ComponentLoader<C> (...) {
init {
component = componentCache.getComponent(componentId) as C
if (component == null) {
component = creator.createComponent(componentId) as C
componentCache.putComponent(componentId, component)
}
}
...
}

The ComponentManager exposes a createComponentLoader method that the Activities and Fragments are going to use to create the Component that they need to inject later on with the Loader that method returns.

ComponentManagerActivity (which extends from AppCompatActivity) is actually the class that stores and retrieves the ComponentCache when a configuration change occurs. Let’s see the code:

open class ComponentManagerActivity : AppCompatActivity(), ComponentManagerDelegate {   private val componentManager = ComponentManager()   override fun onCreate(savedInstanceState: Bundle?) {        
componentManager.onCreate(lastCustomNonConfigurationInstance as ComponentCache)
super.onCreate(savedInstanceState)
}
override fun onRetainCustomNonConfigurationInstance(): Any {
return componentManager.componentCache
}

What else?

UML Class Diagram of the Activity level architecture where you can use your own Activity

šComponentManagerDelegate is an interface that let you access the ComponentManager.

šComponentCreator is an interface that implements classes that are willing to create a Component (this is used by the ComponentLoader to create an instance if it’s not already created).

If you have a BaseActivity with common logic, you FeatureActivity is going to extend from that one that is extending from the ComponentManagerActivity we saw before.

What about Fragments?

Following the same principle, if you have a Fragment without a ViewModel you can extend the BaseFragment straight away.

Press enter or click to view image in full size
UML Class Diagram of the Fragment level architecture to survive configuration changes using a Fragment without VM

The ComponentCreator and the ComponentManagerDelegate of the BaseFragment is going to be the BaseActivity so we reuse the same BaseComponent.

If you need a Fragment with a ViewModel which is lifecycle aware, then you have to extend from the LifecycleAwareFragment which is going to handle the VM lifecycle for you.

UML Class Diagram of the Fragment level architecture to survive configuration changes using a Fragment with VM

Real Use Case

Let’s say you are implementing a NewFeature and you have a NewFeatureFragment and a NewFeatureViewModel. How does the Fragment create the Component it needs to inject?

class NewFeatureFragment : LifecycleAwareFragment(), NewFeatureContract.View {   @Inject override lateinit var viewModel: NewFeatureContract.Actions   override fun createComponents() {        
super.createComponents()
newFeatureComponentLoader = componentManager.createComponentLoader(Constants.COMPONENT_NEW_FEATURE_ID, this)
}
override fun createComponent(id: Int): Any? {
when (id) {
Constants.COMPONENT_NEW_FEATURE_ID -> {
return DaggerNewFeatureComponent.builder()
.applicationComponent(applicationComponent)
.build()
}
}
return super.createComponent(id)
}
override fun injectDependencies() {
newFeatureComponentLoader.component?.inject(this)
super.injectDependencies()
}
}

We override few methods:

  • createComponents is the method where we create the ComponentLoader with the NewFeature ID and this Fragment as a callback (since the BaseFragment implements ComponentCreator).
  • If it needs to create the Component, then it’s going to call the createComponent(Int) method with the ID of the component it needs to create. This fragment has the responsibility for the ID COMPONENT_NEW_FEATURE_ID.
  • We have to inject the Fragment within the injectDependencies() method as we used to do. As you can see, we use the ComponentLoader to retrieve the Component that we need.

Conclusion

Surviving configuration changes is not an easy task, we all know that.

If you want a custom solution that you can fully control, check this article out and get some inspiration.

Kudos to my former colleague Chris Anderson who used to work with me at Capital One UK for collaborating with me to make this happen.

Thanks for reading,

Manuel Vicente Vivo

--

--