Реализуйте состояния загрузки, содержимого и ошибок с поддержкой действия повтора.

Давайте рассмотрим этот вариант использования:

У нас есть операция, которая может занять некоторое время и может быть успешной или неудачной. В случае сбоя мы хотим разрешить пользователям повторить операцию, нажав кнопку повтора.

Итак, мы проектируем наш ViewState следующим образом:

onRetryClicked() в состоянии Error отправит событие в наш поток событий обновления. Сопоставление этой функции не отображается в этом сообщении, потому что оно не связано, но вы можете думать об этом как о публикации Unit в PublishSubject.

Мы пишем нашу ViewState обработку так:

Что тут происходит:

  1. Наш upstream - это бесконечный поток, излучающий сущности нашего домена
  2. Сопоставление сущностей домена с ViewEntity.
  3. Сопоставление ViewEntity с ViewState.Content.
  4. Публикация ViewState.Loading, когда мы подписываемся на этот Steam.
  5. Когда мы получаем сообщение об ошибке, мы создаем ViewState.Error и отправляем его в нашу LiveData.
  6. retryWhen() ничего не делает при получении элемента. Но когда он получает ошибку, он не позволяет ей распространяться ниже по потоку. Вместо этого он начинает слушать torefreshSignalStream и, когда появляется элемент, повторно подписывается на восходящий поток.
  7. Мы подписываемся на наши LiveData и публикуем их. У нас также есть обработка ошибок в подписке, потому что retryWhen() отправит ошибку refreshSignalStream в нисходящем направлении.

Хотя этот способ обработки ViewStates работает, наш поток не передает полный ViewStates. Он излучает только ViewState.Content. Loading и Error ViewState обрабатываются как побочные эффекты (с использованием doOnError() и doOnSubscribe() ), но на самом деле они не являются побочными эффектами. Вы не можете использовать это, если у вас есть другой поток, который зависит от этого ViewState потока.

Давайте улучшим наш поток, выполнив всего ViewStates в том же потоке, а не будем рассматривать некоторые из них как побочный эффект.

Исправление ViewState.Loading

Мы легко можем заменить doOnSubscribe() на startWith(). Тот же эффект будет, когда мы повторно подпишемся на этот Steam.

upstream
    .map(viewEntityMapper)
    .map<ViewState> { ViewState.Content(it) }
    .startWith(ViewState.Loading)
    ...

Исправление ViewState.Error

Когда мы хотим выдать другой элемент при возникновении ошибки, мы можем использовать onErrorResumeNext(aNewStream). Это не позволит ошибке распространиться вниз по потоку, но заменит поток новым потоком, который мы предоставляем.

Мы будем использовать эквивалент onErrorReturnItem(anotherItem), потому что у нас есть только один элемент (это ViewState.Error), и это проще.

Кроме того, если вы хотите зарегистрировать ошибку, вам нужно сделать это до onErrorReturnItem(), потому что это не позволяет ошибке перейти вниз по течению.

Мы добавили doOnError() для регистрации и onErrorReturnItem() для выдачи ViewState.Error:

upstream
    .map(viewEntityMapper)
    .map<ViewState> { ViewState.Content(it) }
    .startWith(ViewState.Loading)
    .doOnError { Timber.e(it, "Error while executing operation.") }
    .onErrorReturnItem(createErrorState())

Теперь повторная часть. Это сложная часть, потому что retryWhen() вступает в игру, когда возникает ошибка. Но у нас не будет ошибки, потому что у нас есть onErrorReturnItem(), который не позволяет ошибке уйти вниз по течению .

К счастью, у retryWhen() есть сестра по имени repeatWhen() . Разница в том, что retryWhen() слушает события ошибок, а repeatWhen() слушает события завершения.

Это идеально, потому что при возникновении ошибки onErrorReturnItem() выдаст только один элемент и затем завершится. Наш ViewState.Error пройдет по цепочке и будет размещен в наших LiveData.

Событие завершения, которое происходит после ViewState.Error, вызовет repeatWhen() ,, и мы начнем прослушивать события обновления, поступающие из refreshSignalStream. Пользователи увидят кнопку повтора на экране (из-за ViewState.Error), и когда они нажмут кнопку refreshSignalStream , она выдаст сообщение, и мы повторно подпишемся на наш апстрим, начиная с ViewState.Loading.

Наша финальная цепочка выглядит так:

Я думаю, что этот способ лучше, потому что теперь у нас есть поток, который испускает полные ViewStates. Другие операции, необходимые для прослушивания этих ViewStates, теперь могут это делать, потому что ни одно из ViewState не обрабатывается иначе.

Это все для этого поста. Дайте мне знать, что вы думаете об этом в комментариях!