Как стать автором
Обновить

Комментарии 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 с таким подходом?

Простите. Не уточнил. Имелось ввиду, что если с помощью FlowBindings реализовать подход пассивных view и им подобные. То есть сделать то же самое, что в примерах.
С одной стороны удобно, view можно переиспользовать. С другой — презентер жестко связан с тем набором методов и контролов, которые декларирует view и к другой view с другой механикой его уже не подключишь.
Как примеры жестких ситуаций — смена конфигурации или версия для AndroidTV. В обоих случаях может кардинально меняться не только интерфейс, но и механика работы. В идеале, об этих нюансах презентеру лучше не знать.
Притягивал реактивность в крупное десктопное приложение. Игру PopulationZero.
Прокомментирую сразу один момент:
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 и "складывать" ресурс с контейнером.

Это только частично закрывает проблему. Я со всем этим хозяйством живу в Unity и у меня соответствующий параметр требуют все расширения, таким образом полностью покрывая весь процесс разборки связей. Например, View получает некоторый viewModel, в параметрах которых лежат нужные данные. И мне нужно не только их забиндить, но и проделать простейшую обработку. У меня это выглядит так:
public override void Connect(MyModel model) {
    model.count
        .Zip(CD, mode.max)
        .Func(CD, (count, max) => count + '/' + max)
        .Bind(CD, targetTextField);
}

А там, где создаётся модель у неё тоже может быть свой DC который по времени жизни совпадает с моделью. Соответственно все потоковые вычисления, которые были сделаны чтобы создать модель гарантированно будут уничтожены вместе с нею. И статическая проверка не даёт что-либо где-либо забыть.

а что мешает в вашем примере использовать стандартный метод вместо кастомного?

или EBA )

Хорошо, конечно, что вы все что-то свое написали, но мне бы просто открыть диалог реактивно)

Есть понимание, что любая ошибка приведёт к отписки от «клик-потока»?

Да, понимание есть. На самом деле, на мой взгляд, это одна из проблем терминальных состояний в 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
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.