RxJava2 + Retrofit 2. Модифицируем адаптер для обработки состояния отсутствия интернета на Android +9




Довольно часто необходимо делать повторные запросы в сеть, например, когда у пользователя не было интернета и он захотел получить данные из интернета. Неплохо бы было заново кинуть запрос при его появлении. Хорошая практика — показать пользователю определенный UI, который объяснил бы ему что произошло и позволил бы заново кинуть запрос. Добавление такой логики может быть довольно болезненной, особенно, когда у нас огромное множество ViewModel классов. Конечно, можно реализовать логику резапросов в каждом ViewModel классе, но это не удобно и возникает огромная вероятность появления ошибок.

Есть ли способ сделать эту обработку лишь единожды?


К счастью RxJava2 и Retrofit2 позволяют это сделать

На Stackoverflow уже есть несколько решений:

1. Создание собственного CallAdapterFactory (подробнее тут)
2. Повтор цепочки, используя PublishSubject (подробнее тут)

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

Реализация


Давайте создадим простой проект. Расположим на главном экране две вкладки. На каждой из них отображен текст, который будет показывать сколько элементов загружено по API. Если в ходе выполнения возникнет ошибка, мы отобразим SnackBar с кнопкой Try Again.



Определим такие базовые классы как BaseActivity, BaseFragment, BaseViewModel, они необходимы для реализации логики повторения запроса в одном месте и избегания дублирования этого кода. Создадим два фрагмента, которые будут расширять BaseFragment. Каждый размещенный фрагмент имеет собственный ViewModel и самостоятельно делает запросы к API. Я создал эти фрагменты, чтобы показать, что при возникновении ошибки каждый запрос будет повторен. Затем создадим фабрику RxRetryCallAdapterFactory, которая расширяет CallAdapter.Factory. После этого создадим экземпляр RxJava2CallAdapterFactory. Нам необходим этот экземпляр для получения доступа к RxJava2CallAdapter, так как мы не хотим дублировать код, который уже есть в Retrofit библиотеке. Так же давайте создадим статический метод, который будет возвращать экземпляр нашей фабрики. Пример кода ниже:

class RxRetryCallAdapterFactory : CallAdapter.Factory() {
    companion object {
        fun create() : CallAdapter.Factory = RxRetryCallAdapterFactory()
    }

    private val originalFactory = RxJava2CallAdapterFactory.create()

    override fun get(returnType : Type, annotations : Array<Annotation>, retrofit : Retrofit) : CallAdapter<*, *>? {
        val adapter = originalFactory.get(returnType, annotations, retrofit) ?: return null
        return RxRetryCallAdapter(adapter)
    }
} 

Далее, создадим RxRetryCallAdapter, который реализовываем интерфейс CallAdapter и нам необходимо передать экземпляр CallAdapter в конструктор. По факту, это должен быть экземпляр RxJava2CallAdapter, который возвращает исходная фабрика.

Далее нам необходимо реализовать следующие вещи:

  • retryWhen оператор используется для внедрения функционала повторения
  • doOnError() оператор, который обрабатывает ошибки. Он используется для отправки трансляции, которая обрабатывается в BaseActivity и показывает SnackBar пользователю
  • PublishSubject используется как триггер событий, который переподписывает цепочку
  • observeOn(Schedulers.io()) оператор, который необходимо применить к PublishSubject (если не добавить это строку, подписка произойдет в основном потоке и мы получим NetworkOnMainThreadException
  • Трансформируем PublishSubject в Flowable и установим BackpressureStrategy.LATEST, так как нам необходима только последняя ошибка

Заметка: Для предоставления PublishSubject я создал простой синглтон класс, который предоставляет все синглтон зависимости в проекте. В реальном проекте вы, вероятно, будете использовать фреймворк внедрения зависимостей, такой как Dagger2

class RxRetryCallAdapter<R>(private val originalAdapter : CallAdapter<R, *>) : CallAdapter<R, Any> {
    override fun adapt(call : Call<R>) : Any {
        val adaptedValue = originalAdapter.adapt(call)
        return when (adaptedValue) {
            is Completable -> {
                adaptedValue.doOnError(this::sendBroadcast)
                        .retryWhen {
                            AppProvider.provideRetrySubject().toFlowable(BackpressureStrategy.LATEST)
                                    .observeOn(Schedulers.io())
                        }
            }
            is Single<*> -> {
                adaptedValue.doOnError(this::sendBroadcast)
                        .retryWhen {
                            AppProvider.provideRetrySubject().toFlowable(BackpressureStrategy.LATEST)
                                    .observeOn(Schedulers.io())
                        }
            }
            //same for Maybe, Observable, Flowable
            else -> {
                adaptedValue
            }
        }
    }

    override fun responseType() : Type = originalAdapter.responseType()

    private fun sendBroadcast(throwable : Throwable) {
        Timber.e(throwable)
        LocalBroadcastManager.getInstance(AppProvider.appInstance).sendBroadcast(Intent(BaseActivity.ERROR_ACTION))
    }
}

Когда пользователь кликает по кнопке Try again мы вызываем onNext PublishSubject. После этого мы переподпишемся на цепочку rx.

Тестирование


Отключим интернет и запустим приложение. Количество загруженных элементов равно нулю на каждом вкладке и SnackBar отображает ошибку. Включаем интернет и кликаем на Try Adain. Через несколько секунд количество загруженных элементов изменяется на каждой из вкладок.



Если кому необходимо, то исходники лежат тут




К сожалению, не доступен сервер mySQL