Сегодня я хотел поделиться с вами еще одним подходом сохранения состояния при разработке android приложений. Не для кого не секрет, что наше приложение в фоне может быть убито в любой момент и эта проблема становится все актуальнее с вводом агрессивного энергосбережения – привет Oreo. Также никто не отменял смену конфигурации на телефоне: ориентация, смена языка и т.д. И чтобы открыть приложение из бэкграунда и отобразить интерфейс в последнем состоянии нам нужно позаботиться о его сохранении. Ох уж этот onSaveInstanceState.

Сколько боли он нам принес.
Далее я буду приводить примеры, ипользуя Clean Achitecture и Dagger2, так что будьте готовы к этому:)
Вопрос сохранения состояния в зависимости от задач можно решить несколькими способами:
Но что делать, если нам нужно восстановить состояние ui, а также текущую реакцию интерфейса на действие пользователя? Для большей простоты рассмотрим решение этой задачи на реальном примере. У нас есть страница логина — пользователь вводит свои данные, нажимает на кнопку и тут к нему поступает входящий звонок. Наше приложение уходит в бэкграунд. Его убивает система. Звучит страшновато, не правда ли?)
Пользователь возвращается к приложению и что он должен увидеть? Как минимум, продолжение операции логина и показ прогресса. Если приложение успело пройти логин до вызова метода onDestroy хоста, то тогда пользователь увидит навигацию на стартовый экран приложения. Данное поведение можно с легкостью решить, используя паттерн состояния (State machine). Очень хороший доклад от яндекс. В этой же статье постараюсь поделиться пережеванными мыслями по этому докладу.
Теперь немного кода:
BaseState
BaseOwner
BaseStateImpl
В нашем случае state owner будет презентер.
Рассматривая страницу логина можно выделить три уникальных состояния:
LoginInitState, LoginProgressingState, LoginCompleteState.
Итак, рассмотрим теперь, что происходит в этих состояниях.
LoginInitState у нас происходит валидация полей и в случае успешной валидации кнопка login становится активной.
В LoginProgressingState делается запрос логина, сохраняется токен, делаются дополнительные запросы для старта главной активити приложения.
В LoginCompleteState осуществляется навигация на главный экран приложения.
Условно переход между состояниями можно отобразить на следующей диаграмме:

Выход из состояния LoginProgressingState происходит в случае успешной операции логина в состояние LoginCompleteState, а в случае сбоя в LoginInitState. Таким образом, когда у нас вьюха детачится, мы имеем вполне детерменированное состояние презентера. Это состояние мы должны сохранить, ис��ользуя стандартный механизм андроида onSaveInstanceState. Для того, чтобы мы могли это сделать, все состояния логина должны имплементировать интерфейс Parcelable. Поэтому расширяем наш базовый интерфейс BaseState.
Далее у нас встает вопрос, как пробросить это состояние из презентера в наш хост? Самый простой способ — из хоста попросить данные у презентера, но с точки зрения архитектуры это выглядит не очень. И поэтому нам на помощь приходят retain фрагменты. Мы можем создать интерфейс для кэша и имплементировать его в таком фрагменте:
Далее мы инжектим кэш фрагмент в кон��труктор интерактора, как Cache. Добавляем методы в интеректоре для получения и сохранения состояния в кэше. Теперь, при каждом изменении состояния презентера, мы можем сохранить состояние в интеракторе, а интерактор сохраняет в свою очередь в кэше. Все становится весьма логично. При первичной загрузке хоста, презентер получает состояние у интерактора, который в свою очередь получает данные из кэша. Так выглядит метод изменения состояния в презентере:
Хочется отметить такой момент — сохранение данных через кэш можно производить для любых данных, не только для состояния. Возможно, вам придется сделать свой уникальный кэш фрагмент для хранения текущих данных. В данной статье рассказан общий подход. Также хочется отметить, что рассматриваемая ситуация очень утрированная. В жизни приходится решать задачи намного сложнее. К примеру, у нас в приложении были совмещены три страницы: логин, регистрация, восстановления пароля. При этом диаграмма состояний выглядела следующим образом:

В итоге, используя паттерн состояний и подход, описанный в статье, нам удалось сделать код более читаемым и поддерживаемым. И что не мало важно — восстанавливать текущее состояние приложения.
Полный код можно посмотреть в репозитории.

Сколько боли он нам принес.
Далее я буду приводить примеры, ипользуя Clean Achitecture и Dagger2, так что будьте готовы к этому:)
Вопрос сохранения состояния в зависимости от задач можно решить несколькими способами:
- Сохранять первичные данные в onSaveInstanceState хоста (Activity, Fragment) — такие как айдишник страницы, пользователя, да что угодно. То, что нам требуется для первичного получения данных и отображения страницы.
- Сохранять полученные данные в интеракторе в репозитории (SharedPreference, Database.
- Использовать ретеин фрагменты для сохранения и восстановления данных при пересоздании активити.
Но что делать, если нам нужно восстановить состояние ui, а также текущую реакцию интерфейса на действие пользователя? Для большей простоты рассмотрим решение этой задачи на реальном примере. У нас есть страница логина — пользователь вводит свои данные, нажимает на кнопку и тут к нему поступает входящий звонок. Наше приложение уходит в бэкграунд. Его убивает система. Звучит страшновато, не правда ли?)
Пользователь возвращается к приложению и что он должен увидеть? Как минимум, продолжение операции логина и показ прогресса. Если приложение успело пройти логин до вызова метода onDestroy хоста, то тогда пользователь увидит навигацию на стартовый экран приложения. Данное поведение можно с легкостью решить, используя паттерн состояния (State machine). Очень хороший доклад от яндекс. В этой же статье постараюсь поделиться пережеванными мыслями по этому докладу.
Теперь немного кода:
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;
}
В нашем случае state owner будет презентер.
Рассматривая страницу логина можно выделить три уникальных состояния:
LoginInitState, LoginProgressingState, LoginCompleteState.
Итак, рассмотрим теперь, что происходит в этих состояниях.
LoginInitState у нас происходит валидация полей и в случае успешной валидации кнопка login становится активной.
В LoginProgressingState делается запрос логина, сохраняется токен, делаются дополнительные запросы для старта главной активити приложения.
В LoginCompleteState осуществляется навигация на главный экран приложения.
Условно переход между состояниями можно отобразить на следующей диаграмме:

Выход из состояния LoginProgressingState происходит в случае успешной операции логина в состояние LoginCompleteState, а в случае сбоя в LoginInitState. Таким образом, когда у нас вьюха детачится, мы имеем вполне детерменированное состояние презентера. Это состояние мы должны сохранить, ис��ользуя стандартный механизм андроида onSaveInstanceState. Для того, чтобы мы могли это сделать, все состояния логина должны имплементировать интерфейс Parcelable. Поэтому расширяем наш базовый интерфейс BaseState.
Далее у нас встает вопрос, как пробросить это состояние из презентера в наш хост? Самый простой способ — из хоста попросить данные у презентера, но с точки зрения архитектуры это выглядит не очень. И поэтому нам на помощь приходят retain фрагменты. Мы можем создать интерфейс для кэша и имплементировать его в таком фрагменте:
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();
}
Далее мы инжектим кэш фрагмент в кон��труктор интерактора, как Cache. Добавляем методы в интеректоре для получения и сохранения состояния в кэше. Теперь, при каждом изменении состояния презентера, мы можем сохранить состояние в интеракторе, а интерактор сохраняет в свою очередь в кэше. Все становится весьма логично. При первичной загрузке хоста, презентер получает состояние у интерактора, который в свою очередь получает данные из кэша. Так выглядит метод изменения состояния в презентере:
@Override
public void setState(@NonNull final LoginBaseState aState){
mState.onExit();
mState = aState;
clearDisposables();
mState.setOwner(this);
mState.onEnter(getView());
mInteractor.setState(mState);
}
Хочется отметить такой момент — сохранение данных через кэш можно производить для любых данных, не только для состояния. Возможно, вам придется сделать свой уникальный кэш фрагмент для хранения текущих данных. В данной статье рассказан общий подход. Также хочется отметить, что рассматриваемая ситуация очень утрированная. В жизни приходится решать задачи намного сложнее. К примеру, у нас в приложении были совмещены три страницы: логин, регистрация, восстановления пароля. При этом диаграмма состояний выглядела следующим образом:

В итоге, используя паттерн состояний и подход, описанный в статье, нам удалось сделать код более читаемым и поддерживаемым. И что не мало важно — восстанавливать текущее состояние приложения.
Полный код можно посмотреть в репозитории.
