📜 ⬆️ ⬇️

State saving in android applications

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 .

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:

  1. 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.
  2. Save the received data in the integrator in the repository (SharedPreference, Database.
  3. 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:

Basestate

public interface BaseState<VIEW extends BaseView, OWNER extends BaseOwner> extends Parcelable{ /** * Get name * * @return name */ @NonNull String getName(); /** * Enter to state * * @param aView view */ void onEnter(@NonNull VIEW aView); /** * Exit from state */ void onExit(); /** * Return to next state */ void forward(); /** * Return to previous state */ void back(); /** * Invalidate view * * @param aView view */ void invalidateView(@NonNull VIEW aView); /** * Get owner * * @return owner */ @NonNull OWNER getOwner(); /** * Set owner * * @param aOwner owner */ void setOwner(@NonNull OWNER aOwner); } 

BaseOwner

 public interface BaseOwner<VIEW extends BaseView, STATE extends BaseState> extends BasePresenter<VIEW>{ /** * Set state * * @param aState state */ 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"); //depend from realization } @Override public void onExit(){ Timber.d(getName()+" onExit"); //depend from realization } @Override public void forward(){ Timber.d(getName()+" forward"); onExit(); //depend from realization } @Override public void back(){ Timber.d(getName()+" back"); onExit(); //depend from realization } @Override public void invalidateView(@NonNull final VIEW aView){ Timber.d(getName()+" invalidateView"); //depend from realization } @NonNull @Override public OWNER getOwner(){ return mOwner; } @Override public void setOwner(@NonNull final OWNER aOwner){ mOwner = aOwner; } 

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:

Login Status Chart

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{ /** * Save cache data * * @param aData data */ void saveCacheData(@Nullable Parcelable aData); @Nullable Parcelable getCacheData(); /** * Check that cache exist * * @return true if cache exist */ 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:

State diagram in a real project

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 .

Source: https://habr.com/ru/post/420023/


All Articles