Today I wanted to share with you another approach to maintaining state when developing android applications. It's no secret that our application in the background can be killed at any time and this problem becomes more urgent with the introduction of aggressive energy saving - hello
Oreo . Also, no one has canceled the configuration change on the phone: orientation, language change, etc. And to open the application from the background and display the interface in the last state, we need to take care of saving it. Oh, this
onSaveInstanceState .

How much pain he brought to us.
Further I will give examples using
Clean Achitecture and
Dagger2 , so be prepared for this :)
')
The question of state preservation depending on the tasks can be solved in several ways:
- Save the primary data in the host's onSaveInstanceState (Activity, Fragment) - such as the user’s page, user, whatever. What we need for the primary data acquisition and display the page.
- Save the received data in the integrator in the repository (SharedPreference, Database.
- Use rethein fragments to save and restore data during re-creation of activation.
But what if we need to restore the state of ui, as well as the current interface response to user action? For greater simplicity, consider the solution to this problem with a real example. We have a login page - the user enters his data, clicks a button and then an incoming call arrives to him. Our application goes to the background. His system kills. It sounds scary, is not it?)
The user returns to the application and what should he see? At a minimum, the continuation of the login operation and showing progress. If the application managed to go through the login before calling the host's onDestroy method, then the user will see the navigation on the application's start screen. This behavior can be easily resolved using the State machine. Very good
report from Yandex . In the same article I will try to share my chewed thoughts on this report.
Now for some code:
Basestatepublic interface BaseState<VIEW extends BaseView, OWNER extends BaseOwner> extends Parcelable{ @NonNull String getName(); void onEnter(@NonNull VIEW aView); void onExit(); void forward(); void back(); void invalidateView(@NonNull VIEW aView); @NonNull OWNER getOwner(); void setOwner(@NonNull OWNER aOwner); }
BaseOwner public interface BaseOwner<VIEW extends BaseView, STATE extends BaseState> extends BasePresenter<VIEW>{ void setState(@NonNull STATE aState); }
BaseStateImpl public abstract class BaseStateImpl<VIEW extends BaseView, OWNER extends BaseOwner> implements BaseState<VIEW, OWNER>{ private OWNER mOwner; @NonNull @Override public String getName(){ return getClass().getName(); } @Override public void onEnter(@NonNull final VIEW aView){ Timber.d( getName()+" onEnter");
In our case, the state owner will be a presenter.
Considering the login page, you can highlight three unique states:
LoginInitState ,
LoginProgressingState ,
LoginCompleteState .
So now consider what happens in these states.
LoginInitState validates the fields and in case of successful validation, the login button becomes active.
In
LoginProgressingState , a login request is made, the token is saved, additional requests are made to start the main activation of the application.
The
LoginCompleteState navigates to the main screen of the application.
Conditionally, the transition between states can be displayed in the following diagram:

The exit from the
LoginProgressingState state occurs in case of a successful login operation in the
LoginCompleteState state, and in the event of a failure in the
LoginInitState . Thus, when we have a view, we have a quite deterministic state of the presenter. We must save this state using the
onSaveInstanceState standard android
mechanism . In order for us to do this, all login states must implement the
Parcelable interface. Therefore, we extend our base
BaseState interface.
Next, we have a question, how to forward this state from the presenter to our host? The easiest way - from the host to ask for data from the presenter, but from the point of view of architecture it does not look very. And so retain fragments come to our rescue. We can create an interface for the cache and implement it in this fragment:
public interface Cache{ void saveCacheData(@Nullable Parcelable aData); @Nullable Parcelable getCacheData(); boolean isCacheExist(); }
Next, we inject the cache fragment into the constructor of the interactor, like Cache. We add methods in the locator for getting and saving the state in the cache. Now, with each change in the state of the presenter, we can save the state in the interactor, and the interactor stores in turn in the cache. Everything becomes quite logical. When the host is initially loaded, the presenter receives a state from the interactor, which in turn receives data from the cache. This is the state change method in the presenter:
@Override public void setState(@NonNull final LoginBaseState aState){ mState.onExit(); mState = aState; clearDisposables(); mState.setOwner(this); mState.onEnter(getView()); mInteractor.setState(mState); }
I would like to note this point - saving data through the cache can be made for any data, not only for the state. You may have to make your unique cache snippet to store current data. This article describes the general approach. I would also like to note that the situation under consideration is very exaggerated. In life, you have to solve problems much more difficult. For example, we have combined three pages in the application: login, registration, password recovery. In this case, the state diagram was as follows:

As a result, using the state pattern and the approach described in the article, we managed to make the code more readable and supported. And what is important is to restore the current state of the application.
The full code can be viewed
in the repository .