State saving in Android MVP with Kotlin delegates
Now you’re probably thinking, “Oh no, not another blog post about MVWhatever. I hoped we left it in 2k17”. Yes, but with Kotlin everything is getting better, so let’s discuss an old topic again.
TL;DR
So, you have a state which has to be stored somewhere. There are different approaches and opinions. It’s up to you to choose the best solution for your specific problem.
In our project we have in memory cache for loaded data and presenters use it via repositories. So usually we don’t need to store anything inside a presenter which is very convenient and easily testable.
But there are cases when we need to keep a state which is used only for presentation logic. It could be too dumb and small to store it in the domain layer or somewhere else in the application scope.
We’d like to have it in the presenter because it’s presenter’s responsibility to deal with presentation logic. But we live in Android and we have to use Bundle
to store the state during activity recreation which is not good because we don’t want to have framework specific classes or callbacks inside our presenters. Therefore for such cases we used stateful presenters in our project.
With this approach presenters have a method like getState()
which is invoked from onSaveInstanceState()
of an Activity or a Fragment.
It has some disadvantages:
- If we use Icepick, the View has magic ugly properties which are used only to initialize presenter.
- A stateful presenter has to be initialized with a state by a View. The state is often
null
during the first launch so we have to implement additionalif-else
branch in the presenter’sinit()
method. - The View knows about the state. It becomes smarter and has more than just rendering logic. It’s difficult to cover with unit tests.
Kotlin Delegates
The following approach was inspired by this blog post (in Russian)
Let’s use the delegated properties feature to solve our problem.
We created the basic delegate class which implements ReadWriteProperty
interface and stores the value of the delegated property in the stateBundle
. It’ll be explained later why we use lazy initialisation.
We need to create two different implementations of the ReadWriteProperty.getValue()
method for nullable and not null types.
The only difference is the base type of the generic.
Let’s create a class which provides the delegates in a convenient way and can be injected to our presenters.
Now we need to apply it for our MVP architecture. First of all we need an interface for a stateful View.
Then add some logic to the BaseActivity
. It’s not much more than we usually add using Icepick.
Note that the BaseActivity
has stateBundle
but doesn’t implement ViewWithState
interface. It allows us to extend BaseActivity
either with stateful or stateless views.
Now let’s implement MVP components.
- In our example the view property is
lateinit
in theBasePresenter
. It means that the delegated propertycount
is initialized before we get the view (during theCounterPresenter
creation). That’s the reason why we used lazy initialisation of thestateBundle
in theInstanceStateProvider
class and why the methods of theInstanceStateDelegates
get theviewProvider
block and not theview
itself. - Inject
instanceState
with Dagger. - BOOM! Now we have a state inside the presenter. It’s stored in a bundle inside the delegate but the presenter knows nothing about framework specific implementation details.
And that’s how our View looks:
CounterActivity
has no state saving specific code and that’s great.
So what do we have?
- We got rid of state in the View layer.
- We keep the presentation state in the presenter and we don’t have Android specific classes there.
- The state specific code in the presenter is clean and null safe.
Bonus: Testing
We inject InstanceStateDelegates
object into presenters so it’s easy to mock and write unit tests: