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:
- Android Architecture Components library by Google.
- ThirtyInch Android library.
- Other libraries…
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()andgetLastCustomNonConfigurationInstance()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)andonDestroy()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
onSaveInstanceStateif 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
onSaveInstanceStatehas been called beforeonDestroy, 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
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?
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.
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.
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:
createComponentsis 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 IDCOMPONENT_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
