Комментарии 19
Почему именно RxJava? Почему не Coroutines + Flow ?
На данном подходе у нас приложение написанное с нуля где-то полтора года назад. Тогда еще Flow если и были, то не в стабильном релизе. Если бы сейчас писали с нуля, то возможно подумали бы и о Flow. Но опять же, не видел достойных примеров, в которых можно на архитектурном уровне их использовать. Кажется, что они решают больше локальные чёткие задачи и в принципе не конфликтуют в рамках одного приложения с Rx.
Кажется, что они решают больше локальные чёткие задачиНе совсем. По факту та же реактивщина, просто из коробки меньше конструктов и операторов. Как заявлено на странице Flow, главные цели — проще дизайн, дружественность к прерываниям и структурному параллелизму.
Да, есть конвертация в RxJava и обратно. Однако, на IO19 было объявлено, что для Jetpack первоочередной будет всё-таки поддержка корутин, а RxJava будет поддерживаться на уровне документации. Там же назвали корутины рекомендованным решением.
То есть те расширения RxJava, которые из коробки есть для Room и других компонентов — не понятно, насколько долго они будут существовать.
Мне нравится подход реактивного UI начиная от пользовательского действия и заканчивая базой и сетью. На тот момент не было ничего похожего на CoroutinesBinding и они были в бете. Поддержка Flow в Room вроде появилась только под Новый год. Использовать сырой подход без понимания того, кто его будет поддерживать пока не хотелось. Но в общем мы вполне рассматриваем использование Flow как замена Rx на архитектурном уровне, хотя и Rx нас всем устраивает
Но многим (включая меня) подобные подходы не нравятся по причине того, что частично или полностью переносят механику view на presenter\view model.
А какая именно механика переносится из view с таким подходом?
С одной стороны удобно, view можно переиспользовать. С другой — презентер жестко связан с тем набором методов и контролов, которые декларирует view и к другой view с другой механикой его уже не подключишь.
Как примеры жестких ситуаций — смена конфигурации или версия для AndroidTV. В обоих случаях может кардинально меняться не только интерфейс, но и механика работы. В идеале, об этих нюансах презентеру лучше не знать.
Прокомментирую сразу один момент:
1) addToDisposable(compositeDisposable) не оптимальное решение, потому что когда у вас много реактивности вот это вот вечно оказывается забыто. Потому что никаких наказаний з а забывание не предусмотрено. Я в своей статье описал более хорошее решение. Все методы по манипулированию потоками типа Subscribe принимают compositeDisposable в качестве обязательного первого параметра. Для удобства этой переменной в формачках придаётся короткое название, к которому все быстро привыкают. В результате вместо:
archView.loginClick()
.subscribe { authNavigationController.goToLogin(LoginParams()) }
.addToDisposable(compositeDisposable)
Вы пишите:
archView.loginClick()
.subscribe(CD, authNavigationController.goToLogin(LoginParams()) )
И вероятность случайной ошибки, когда тупо забыли что-то прибрать снижается почти до нуля. У нас в приложении один раз была связанная с этим ошибка когда программист запутался между временем жизни Awake-Dispose и Connect-Disconnect и всё, больше ошибок не было.
P.S. А собеседование у вас я не прошёл. :)
Метод subscribe
, который возвращает Diposable
, помечен аннотацией CheckReturn
, ничто не мешает линтером проверять, действительно ли результат таких методов был передан куда-то дальше. Студия навязчиво подсвечивает такие ситуации. Я бы предложил переопределить для пары DisposableContainer
и Disposable
инфиксную операторную функцию plus
и "складывать" ресурс с контейнером.
public override void Connect(MyModel model) {
model.count
.Zip(CD, mode.max)
.Func(CD, (count, max) => count + '/' + max)
.Bind(CD, targetTextField);
}
А там, где создаётся модель у неё тоже может быть свой DC который по времени жизни совпадает с моделью. Соответственно все потоковые вычисления, которые были сделаны чтобы создать модель гарантированно будут уничтожены вместе с нею. И статическая проверка не даёт что-либо где-либо забыть.
а что мешает в вашем примере использовать стандартный метод вместо кастомного?
Да, понимание есть. На самом деле, на мой взгляд, это одна из проблем терминальных состояний в Rx и отличает корутины.
Сетевые запросы мы делаем через ретрофит и оборачиваем все в ответы в подобный объект
data class SafeResponse<T> (val data: T?, val exception: Throwable? = null)
Оборачиваем на уровне интерфейса ретрофита через подобие RxJava2CallAdapter.
Ошибка тоже пробрасывается в onNext и не приводит к завершению цепочки.
Из интеракторов тоже частично возвращаем данные в подобном формате.
И обрабатываем их следующим образом
fun <T> Observable<SafeResponse<T>>.safeResponseSubscribe(onData: (data: T) -> Unit = {},
onError: (data: Throwable) -> Unit = {}): Disposable =
this.subscribe({
if (it.data != null) {
onData(it.requireData)
} else {
onError(it.requireException)
}
}, { onError(it) })
Для Observable<SafeResponse> у нас написаны экстеншены для упрощения работы в таком формате.
За счет этого объекта и пробрасывания ошибок в onNext мы как раз избегаем такого.
Статья отличная, к месту бы пришелся кейс с оператором share
, разделение цепочек не практикуете?
Я активно использую Rx для синхронизации изменений из базы данных. Большую часть кода занимает обработка ошибок. Если это не простейшая ошибка сети, а что-то разумное, то нужно предпринять меры для достижения согласованности локальной базы данных. Для этого в обработчике ошибок от сервера нужно иметь ссылку на модель, которая не была успешно отправлена.
С Rx это выливается во вложенные лямбды или выделенные реализации onErrorResumeNext
и отдельный класс исключение, чтобы передать модель. Вроде этого
class SyncCoherenceError(val model: SyncModel): Exception()
fun sync(bundle: SyncModel): Single<SuccessModel> {
return api.sync(bundle)
.onErrorResumeNext { error ->
if (error is CoherenceError) {
Single.error(SyncCoherenceError(bundle))
} Single.error(error)
}
}
В обработчике нужно "распаковать" исключение, снова onErrorResumeNext
. Никак не доходят руки проверить облегчат ли задачу корутины с им императивным стилем.
Заметил, что во многих местах вы используете промежуточный BehaviorSubject вместо использования соответствующего метода Observable.create
Также, имхо, в методах типа createSimpleDialog логичнее смотрится результатом объект типа Single или Maybe
Rx головного мозга