Preserving Grox State with Dependency Injection
Device configuration changes during runtime are inevitable and one of the common configuration changes is device rotation. Android makes it easier for the application to handle device rotation by restarting the running activity. We have seen in our previous¹ articles² that Grox is very good at managing the applications internal state. However, Grox does not handle device rotation and as a result, we lose the Grox store. In this article, we will see how we can use Grox and Dependency Injection to save the Grox store on device rotation.
Let’s take an example
Our sample application is simple: a text view with a number, a + button, and a - minus button to increment and decrement the counter respectively.
Let’s go through some code for the
SimpleActivity and see how it works:
This code is fairly simple. However, there is an issue here: if we rotate the device the current counter value will disappear. The device rotation will destroy the activity and because our Grox store is a field it will get destroyed too. So, how do we survive the device rotation without losing our Grox store?
Dependency Injection (DI) to the rescue
One way to preserve the state and survive rotation is to save the Grox store in a location which is unaffected by the configuration change. DI scopes provide the safe haven to survive rotation. Toothpick (TP) is our DI framework of choice and we will use it for scope creation and dependency injection. First, we will go through some DI terminologies that are very specific to Toothpick.
A scope in Toothpick is simply a hash map: it maps a class
State to a provider of
State. A Provider is an entity that knows how to create instances of
State. The association of the key to a provider is called a binding.
For instance, we can have a scope with a binding like
State.class -> Provider<State>. The scope can be used to perform injections as seen below:
Toothpick.inject()is called, Toothpick will provide an instance of the class
State, and assign it to the field
state of the activity’s instance. To create this instance of
State, the scope will use its binding.
ToothPick Scope Tree
Scopes can be created and destroyed on demand, and they are organized into a tree. In a typical Android application, we create the following scope tree:
The application scope is a scope that will have the same lifecycle as the Android application itself. An activity scope is a scope that lives as long as an activity is living. Activity scope is created during activity’s
onCreate() and destroyed when the activity dies. All children scopes have access to the bindings of their parent scopes.
Toothpick Binding and Scope Creation
Toothpick provides methods to create and destroy scopes and managing scope tree branches easily.
With the line above, we open 2 scopes. One for the application, and one for the activity which will be a child of the application scope. The method returns the leaf node. The parameters passed to this method are the names of the scopes.
Once a scope is created, we can install modules that contain bindings:
Now, our scope is ready to perform the injection of
MyActivityfrom the earlier section. Every time Toothpick detects
@Inject lateinit var state: State, it will replace this field with the instance of the state that was bound in the module. Because we used
toInstance() to create this binding, it is de facto a singleton inside this scope.
The activity scope will also have to be destroyed when the activity is destroyed:
Preserving the Grox Store
Now that we know how to use DI scopes, let’s see how to use them to solve our initial issue and make our Grox Store survive the rotation of the activity.
First, we are going to create a scope that survives the rotation of the activity. The application scope could be a good candidate but if we put our Grox Store there, it means that it will never be garbage collected during the application lifespan.
An intermediate scope between the application scope and the activity scope is a better candidate: we can have a fine-grained control over its creation and destruction. Let’s see how to create such a scope and put our store in this scope.
First we will look at the scope tree for our persistent Grox store sample before we visit the code:
- We have the application scope, intermediate scope and the activity scope in our scope tree.
- On configuration change, we close the activity scope.
- On recreation, we open the activity scope and attach it to the intermediate scope in our scope tree.
We saw the scope tree for our sample, now let’s see some code to understand the creation of the scope tree.
- First we call
Toothpick.openScopes()to open the application scope and the intermediate scope. Initially, it creates the application scope followed by the intermediate scope and installs modules into the intermediate scope.
- Thanks to our binding, the store is now bound inside the intermediate scope, not in the activity scope After that, we open the activity scope as a child of the intermediate scope.
Toothpick.inject()will inject the instance of the
PersistStorethat we provided through the binding.
Don’t forget to destroy the scope
Make sure to destroy the
INTERMEDIATE_SCOPE when the activity is destroyed normally i.e by pressing the back button or calling
finish(). These give us an opportunity to close the scope and release the Grox store. Below is the code from our sample to show how we take care of the scope destruction.
We saw that creating DI scopes is really easy in Toothpick. An intermediate scope is a good candidate to preserve the Grox store during device rotation. Thus, the activity can reload the UI state by subscribing to the existing Grox store. Although this approach survives device rotation, it is prone to system initiated process death. We suggest that you use persistent storage to avoid state loss on process death. With 💚 Pratyush Kshirsagar from the the Groupon Android team.