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 an 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.

Next, I will give examples using Clean Achitecture and Dagger2 , so be prepared for this :) The

issue 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 data in the integrator in the repository (SharedPreference, Database.
  3. Use rethein fragments for saving and restoring data when recreating activations.

But what to do if we need to restore the state ui, as well as the current interface response to the user's 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

publicinterfaceBaseState<VIEWextendsBaseView, OWNERextendsBaseOwner> extendsParcelable{
    /**
     * Get name
     *
     * @return name
     */@NonNullString getName();
    /**
     * Enter to state
     *
     * @param aView view
     */voidonEnter(@NonNull VIEW aView);
    /**
     * Exit from state
     */voidonExit();
    /**
     * Return to next state
     */voidforward();
    /**
     * Return to previous state
     */voidback();
    /**
     * Invalidate view
     *
     * @param aView view
     */voidinvalidateView(@NonNull VIEW aView);
    /**
     * Get owner
     *
     * @return owner
     */@NonNullOWNER getOwner();
    /**
     * Set owner
     *
     * @param aOwner owner
     */voidsetOwner(@NonNull OWNER aOwner);
}

BaseOwner

publicinterfaceBaseOwner<VIEWextendsBaseView, STATEextendsBaseState> extendsBasePresenter<VIEW>{
    /**
     * Set state
     *
     * @param aState state
     */voidsetState(@NonNull STATE aState);
}

BaseStateImpl

publicabstractclassBaseStateImpl<VIEWextendsBaseView, OWNERextendsBaseOwner> implementsBaseState<VIEW, OWNER>{
    private OWNER mOwner;
    @NonNull@Overridepublic String getName(){
        return getClass().getName();
    }
    @OverridepublicvoidonEnter(@NonNull final VIEW aView){
        Timber.d( getName()+" onEnter");
        //depend from realization
    }
    @OverridepublicvoidonExit(){
        Timber.d(getName()+" onExit");
        //depend from realization
    }
    @Overridepublicvoidforward(){
        Timber.d(getName()+" forward");
        onExit();
        //depend from realization
    }
    @Overridepublicvoidback(){
        Timber.d(getName()+" back");
        onExit();
        //depend from realization
    }
    @OverridepublicvoidinvalidateView(@NonNull final VIEW aView){
        Timber.d(getName()+" invalidateView");
        //depend from realization
    }
    @NonNull@Overridepublic OWNER getOwner(){
        return mOwner;
    }
    @OverridepublicvoidsetOwner(@NonNull final OWNER aOwner){
        mOwner = aOwner;
    }

In our case, the state owner will be a presenter.

Considering the login page, three unique states can be distinguished:

LoginInitState , LoginProgressingState , LoginCompleteState .

So now consider what happens in these states.

LoginInitState validates the fields and if the validation is successful, 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 case 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 therefore retain fragments come to our aid. We can create an interface for the cache and implement it in this fragment:

publicinterfaceCache{
    /**
     * Save cache data
     *
     * @param aData data
     */voidsaveCacheData(@Nullable Parcelable aData);
    @NullableParcelable getCacheData();
    /**
     * Check that cache exist
     *
     * @return true if cache exist
     */booleanisCacheExist();
}

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:

@OverridepublicvoidsetState(@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 own unique cache snippet to store current data. This article describes the general approach. I would also like to note that this situation is very exaggerated. Life has to solve problems much more difficult. For example, we have combined three pages in the application: login, registration, password recovery. At the same time, 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 maintainable. And what is important is to restore the current state of the application.

The full code can be viewed in the repository .

Also popular now: