CodeFest 2019. Дмитрий Тримонов (Яндекс) — Rx в современной мобильной разработке. Есть ли смысл?

16b6c87229eaf58768d25ed7b2bbbf52?s=47 CodeFest
April 06, 2019

CodeFest 2019. Дмитрий Тримонов (Яндекс) — Rx в современной мобильной разработке. Есть ли смысл?

В докладе речь пойдет о целесообразности использования ФРП (Functional Reactive Programming) в современной мобильной разработке на примере RxSwift под iOS. Еще год назад мне казалось, что невозможно писать код без использования Rx, а все самописные велосипеды, призванные в очередной раз реализовать паттерн Observer — от лукавого. Во всех проектах, в которых мне довелось поучаствовать, в числе которых две версии Яндекс.Карт под Windows Phone и iOS, Rx стал одной из главных библиотек, насквозь пронизывающей все слои приложения. В последнее время моё отношение к Rx стало гораздо более прагматичным в силу ряда причин, о которых я расскажу в докладе.

Цель моего доклада — поглубже посмотреть на устройство одного из самых популярных фреймворков для ФРП под iOS — RxSwift и на его примере показать, что Rx как и любой инструмент — это лишь удобный способ решения определенного спектра задач. Мы поговорим о best practices при использовании Rx, тонкостях использования различных компонентов любого современного Rx фреймворка, таких как Subject, Scheduler, Trait и других, рассмотрим способы тестирования сложных time-dependent фич, коснемся вопросов о контрактах Rx и многом другом, а главное, попытаемся понять, помогут ли все эти знания сделать наш проект более понятным, поддерживаемым и bug-free.

16b6c87229eaf58768d25ed7b2bbbf52?s=128

CodeFest

April 06, 2019
Tweet

Transcript

  1. 1

  2. Тримонов Дмитрий Rx в современной мобильной разработке. Есть ли смысл?

    2
  3. Введение в Reactive Extension › Push vs Pull, FRP, история

    создания Rx - отличный инструмент › пишем тестируемый реактивный код › делаем тесты быстрыми Проблемы при использовании › гайдлайны vs реальность › устройство фреймворка План 3
  4. Введение в Reactive Extensions 4

  5. Pull - запрашиваем следующую порцию данных как только они нам

    понадобились › Iterator Push - получаем оповещение о новой порции данных от поставщика как только данные готовы › Delegate › Observer Push vs Pull подходы 5
  6. Функциональное › иммутабельные структуры данных › «чистые функции» - функции

    без side-эффектов Реактивное › данные - потоки событий с аргументами во времени (aka streams) › синхронный и асинхронный код воспринимается одинаково FRP как структура приложения 6
  7. Откуда появился Observable 7 один элемент несколько элементов синхронно T

    getItem() Enumerable<T> getItems() асинхронно Task<T> getItem() ???
  8. Откуда появился Observable 8 один элемент несколько элементов синхронно T

    getItem() Enumerable<T> getItems() асинхронно Task<T> getItem() Observable<T> getItems()
  9. Дуальность на примере C# 9 IEnumerable / IEnumerator IObservable /

    IObserver
  10. Мантра реактивного программирования 10

  11. 〉NotificationCenter 〉Delegate patter 〉Grand Central Dispatch 〉Closures Зачем может понадобиться

    Rx 11 Унификация при работе с различными асинхронными API
  12. › Microsoft Reactive Extensions (Rx .NET) - 2009 › Сообщество

    (reactivex.io) определило общее API для имплементаций на разных языках программирования › RxSwift 2015 История создания 12
  13. ReactiveSwift, ReactiveCocoa › решает некоторые проблемы Reactive Extensions API ReSwift

    › накладывает жесткие ограничения на способ написания приложения PromiseKit › другая абстракция Альтернативы RxSwift 13
  14. Rx - мощный инструмент! 14

  15. Простота внедрения › возможность поэтапного внедрения › порог вхождения Ускорение

    процесса разработки › упрощение реализации фичей › повышение читаемости, тестируемости, … кода Сложность отказа/замены › зависимость проекта от «предоставляемых услуг» Как оценить инструмент? 15
  16. Простота внедрения › возможность поэтапного внедрения › порог вхождения Ускорение

    процесса разработки › упрощение реализации фичей › повышение читаемости, тестируемости, … кода Сложность отказа/замены › зависимость проекта от «предоставляемых услуг» Как оценить инструмент? 16
  17. Пишем тестируемый реактивный код

  18. Автоматическое скрывание шторки

  19. Состояния шторки 19

  20. Пользователь должен видеть карту при быстром движении

  21. Production 21

  22. Условия автоматического скрывания › Пользователь превысил скорость 2.5 м/с при

    включенном режиме автовращения › Пользователь включил режим автовращения при скорости, превышающей 2.5 м/с Авто-скрывание шторки 22
  23. Возвращает Observable событий, когда нужно закрыть шторку по простой логике

    Реализация func hideEvents(didChangeAutoRotationMode: Observable<Bool>, didUpdateSpeed: Observable<Double>) -> Observable<Void> { let speedIsAboveThreshold = didUpdateSpeed.map { $0 > 2.5 } .distinctUntilChanged() let autoRotationIsOn = didChangeAutoRotationMode .distinctUntilChanged() return Observable.combineLatest(speedIsAboveThreshold, autoRotationIsOn) .filter { $0.0 && $0.1 } .mapTo(()) }
  24. 24

  25. Тестирование Rx API Выглядит неплохо, а как протестировать? Ответ прост

    - TestScheduler 25
  26. createHotObservable › создает стрим по списку событий в абсолютные моменты

    времени createColdObservable › создает стрим по списку событий в относительные моменты времени start › создает TestObserver › создает Observable, подписывается и уничтожает подписку в моменты времени: created, subscribed, disposed TestScheduler 26
  27. TestScheduler.start 27 t
 tcreate
 Создание Observable tsubscribe
 Подписка tdispose
 Отписка

    Создание TestObserver scheduleAbsoluteVirtual(_:time) для каждого t
  28. Тесты простой логики func testWhenSpeedExceedsThresholdAndAutoRotationIsOnThenDrawerHides() { let autoRotationMode = testScheduler.createHotObservable(

    [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SimpleDrawerHidingStrategy().hideEvents( didChangeAutoRotationMode: autoRotationMode.asObservable(), didUpdateSpeed: speed.asObservable() ) } XCTAssert(hideEventsObserver.events.count == 1) XCTAssert(hideEventsObserver.events[0].time == 800) }
  29. Тесты простой логики func testWhenSpeedExceedsThresholdAndAutoRotationIsOnThenDrawerHides() { let autoRotationMode = testScheduler.createHotObservable(

    [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SimpleDrawerHidingStrategy().hideEvents( didChangeAutoRotationMode: autoRotationMode.asObservable(), didUpdateSpeed: speed.asObservable() ) } XCTAssert(hideEventsObserver.events.count == 1) XCTAssert(hideEventsObserver.events[0].time == 800) }
  30. Тесты простой логики func testWhenSpeedExceedsThresholdAndAutoRotationIsOnThenDrawerHides() { let autoRotationMode = testScheduler.createHotObservable(

    [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SimpleDrawerHidingStrategy().hideEvents( didChangeAutoRotationMode: autoRotationMode.asObservable(), didUpdateSpeed: speed.asObservable() ) } XCTAssert(hideEventsObserver.events.count == 1) XCTAssert(hideEventsObserver.events[0].time == 800) }
  31. Усложним первое условия автоматического скрывания › Пользователь превысил скорость 2.5

    м/с при включенном режиме автовращения, после чего прошло 5 секунд, в течение которых скорость не падала ниже 2.5 м/с и режим автовращения не был выключен Авто-скрывание шторки 31
  32. 32

  33. Реализация сложной логики func hideEvents(didChangeAutoRotationMode: Observable<Bool>, didUpdateSpeed: Observable<Double>) -> Observable<Void>

    { let autoRotationIsOn = didChangeAutoRotationMode .distinctUntilChanged() let autoRotationDidTurnOn = autoRotationIsOn.filter { $0 } let autoRotationDidTurnOff = autoRotationIsOn.filter { !$0 } let speedIsAboveThreshold = didUpdateSpeed .map { $0 > 2.5 } .distinctUntilChanged() let speedUp = speedIsAboveThreshold.filter { $0 } let speedDown = speedIsAboveThreshold.filter { !$0 } ...
  34. Реализация сложной логики func hideEvents(didChangeAutoRotationMode: Observable<Bool>, didUpdateSpeed: Observable<Double>) -> Observable<Void>

    { let autoRotationIsOn = didChangeAutoRotationMode .distinctUntilChanged() let autoRotationDidTurnOn = autoRotationIsOn.filter { $0 } let autoRotationDidTurnOff = autoRotationIsOn.filter { !$0 } let speedIsAboveThreshold = didUpdateSpeed .map { $0 > 2.5 } .distinctUntilChanged() let speedUp = speedIsAboveThreshold.filter { $0 } let speedDown = speedIsAboveThreshold.filter { !$0 } ...
  35. Реализация сложной логики ... let speedUpWhileAutoRotationIsOn = speedUp .withLatestFrom(autoRotationIsOn) .filter

    { $0 } let timerFor5Sec = Observable<Int>.timer(5.0, scheduler: ...) let timeShouldStop = Observable<Void> .merge(autoRotationDidTurnOff, speedDown) let speedCondition = speedUpWhileAutoRotationIsOn .flatMapLatest { _ -> Observable<Void> in return timerFor5Sec.takeUntil(timeShouldStop) } ... return Observable.merge(autoRotationCondition, speedCondition) }
  36. Реализация сложной логики ... let speedUpWhileAutoRotationIsOn = speedUp .withLatestFrom(autoRotationIsOn) .filter

    { $0 } let timerFor5Sec = Observable<Int>.timer(5.0, scheduler: ...) let timeShouldStop = Observable<Void> .merge(autoRotationDidTurnOff, speedDown) let speedCondition = speedUpWhileAutoRotationIsOn .flatMapLatest { _ -> Observable<Void> in return timerFor5Sec.takeUntil(timeShouldStop) } ... return Observable.merge(autoRotationCondition, speedCondition) }
  37. Реализация сложной логики ... let speedUpWhileAutoRotationIsOn = speedUp .withLatestFrom(autoRotationIsOn) .filter

    { $0 } let timerFor5Sec = Observable<Int>.timer(5.0, scheduler: ...) let timeShouldStop = Observable<Void> .merge(autoRotationDidTurnOff, speedDown) let speedCondition = speedUpWhileAutoRotationIsOn .flatMapLatest { _ -> Observable<Void> in return timerFor5Sec.takeUntil(timeShouldStop) } ... return Observable.merge(autoRotationCondition, speedCondition) }
  38. Проверяем, что шторка закроется именно через 5 секунд Тесты сложной

    логики let autoRotationMode = testScheduler.createHotObservable( [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SmartDrawerHidingStrategy().hideEvents(...) } XCTAssert(hideEventsObserver.events[0].time == 805)
  39. Тесты сложной логики Проверяем, что шторка не закроется при падении

    скорости let autoRotationMode = testScheduler.createHotObservable( [.next(300, false), .next(600, true)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51), .next(804, 2.49)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SmartDrawerHidingStrategy().hideEvents(...) } XCTAssert(hideEventsObserver.events.isEmpty)
  40. Тесты сложной логики Проверяем, что шторка не закроется при выключении

    автовращения let autoRotationMode = testScheduler.createHotObservable( [.next(300, false), .next(600, true), .next(804, false)] ) let speed = testScheduler.createHotObservable( [.next(400, 1.5), .next(800, 2.51)] ) let hideEventsObserver = testScheduler.start { () -> Observable<Void> in return SmartDrawerHidingStrategy().hideEvents(…) } XCTAssert(hideEventsObserver.events.isEmpty)
  41. Реакция на движения карты при поиске

  42. Должны реагировать на движения карты › Поиск должен начинаться только

    после завершении анимации камеры (drag, pinch, …) › Поиск должен отменяться при старте анимации в момент поиска Поиск в картах 42
  43. 43

  44. Абстрагируем способ получения событий об изменении позиции камеры и способ

    поиска Протоколы protocol Map: AnyObject { var mapCameraEvents: Observable<MapCameraEventArgs> { get } } protocol MapKudaGoSearchAPI { func searchEvents(with ...) -> Observable<Result<[KudaGoEvent]> }
  45. Возвращает Observable событий об изменении состояния экрана MapKudaGoSearchViewModel class MapKudaGoSearchViewModel

    { var didChangeScreenState: Observable<ScreenState> { return screenState.asObservable() } init(map: Map, searchApi: MapKudaGoSearchAPI) { ... } ... private let screenState = PublishRelay<ScreenState>() }
  46. Реализация логики поиска let stops = map.mapCameraEvents.filter { $0.state ==

    .finished } let starts = map.mapCameraEvents.filter { $0.state == .started } let searchRequests = stops.debounce(0.5) .withLatestFrom(map.mapCameraEvents) .filter { $0.state != .started } searchRequests .do(onNext: { _ in screenState.accept(.loading) }) .flatMapLatest { [weak self] args in let cancels = starts.do(onNext: { screenState.accept(.canceled) }) return self?.searchApi.searchEvents(args).takeUntil(cancels) ?? ... } .map { result -> ScreenState in ... } .bind(to: screenState)
  47. Тесты логики отмены поиска Проверяем, что поиск отменяется при старте

    анимации камеры let mapCameraEvents = testScheduler.createHotObservable([ .next(300, MapCameraEventArgs(mapCamera: ..., state: .finished)), .next(400, MapCameraEventArgs(mapCamera: ..., state: .started)) ]) let searchResult = testScheduler.createHotObservable([ .next(500, Result<[KudaGoEvent], APIError>.success([])) ]) let sut = MapKudaGoSearchViewModel(...) let stateObserver = testScheduler.start { () -> Observable<ScreenState> in return sut.didChangeScreenState } XCTAssert(stateObserver.events[1] == .next(400, ScreenState.canceled))
  48. Тесты логики отмены поиска Проверяем, что поиск отменяется при старте

    анимации камеры let mapCameraEvents = testScheduler.createHotObservable([ .next(300, MapCameraEventArgs(mapCamera: ..., state: .finished)), .next(400, MapCameraEventArgs(mapCamera: ..., state: .started)) ]) let searchResult = testScheduler.createHotObservable([ .next(500, Result<[KudaGoEvent], APIError>.success([])) ]) let sut = MapKudaGoSearchViewModel(...) let stateObserver = testScheduler.start { () -> Observable<ScreenState> in return sut.didChangeScreenState } XCTAssert(stateObserver.events[1] == .next(400, ScreenState.canceled))
  49. Тесты логики отмены поиска Проверяем, что поиск отменяется при старте

    анимации камеры let mapCameraEvents = testScheduler.createHotObservable([ .next(300, MapCameraEventArgs(mapCamera: ..., state: .finished)), .next(400, MapCameraEventArgs(mapCamera: ..., state: .started)) ]) let searchResult = testScheduler.createHotObservable([ .next(500, Result<[KudaGoEvent], APIError>.success([])) ]) let sut = MapKudaGoSearchViewModel(...) let stateObserver = testScheduler.start { () -> Observable<ScreenState> in return sut.didChangeScreenState } XCTAssert(stateObserver.events[1] == .next(400, ScreenState.canceled))
  50. Тесты логики отмены поиска Проверяем, что поиск отменяется при старте

    анимации камеры let mapCameraEvents = testScheduler.createHotObservable([ .next(300, MapCameraEventArgs(mapCamera: ..., state: .finished)), .next(400, MapCameraEventArgs(mapCamera: ..., state: .started)) ]) let searchResult = testScheduler.createHotObservable([ .next(500, Result<[KudaGoEvent], APIError>.success([])) ]) let sut = MapKudaGoSearchViewModel(...) let stateObserver = testScheduler.start { () -> Observable<ScreenState> in return sut.didChangeScreenState } XCTAssert(stateObserver.events[1] == .next(400, ScreenState.canceled))
  51. Выделяйте удобные абстракции › входные потоки данных › способ выполнения

    - Scheduler Выполняйте side-эффекты явно › в подписках › используя оператор do 51 Рекомендации
  52. Плюсы › декларативность › однообразность › легко тестировать Минусы ›

    для понимания необходимо сразу знать все операторы › требуется «въехать» в парадигму › сложно отказаться Итоги 52
  53. Делаем тесты быстрыми

  54. Предусловия 54 Старый код, использующий › completions Тесты используют реальное

    время › wait(for: [Expectations]) › dispatch
  55. Желание протестировать асинхронное API приводит к долгим тестам

  56. Используем виртуальное время 56 DispatchWorkItem VirtualSchedulerItem async(after:) scheduleRelative(dueTime:) workItem.cancel() workItem.dispose()

    Unit of work Schedule Cancel DispatchQueue TestScheduler TestScheduler - очередь для выполнения задач в определенные моменты времени, но с виртуальным временем
  57. Рекомендации 57 › Покрывайте асинхронное поведение тестами, использующими виртуальное время

    › Подключайте RxTest в таргет тестов и используйте TestScheduler для получения «быстрых» тестов
  58. Проблемы при использовании Rx 58

  59. Несоответствием концепций и гайдлайнов с реальностью › кэширует ли стрим

    значение? › как получить текущее значение стрима? › как понять, горячий это стрим или холодный? › как показать, что мой Observable не эмитит ошибок? Проблемы вызванные 59
  60. Гайдлайны vs Реальность

  61. Цель контрактов › гарантия определенного поведения у интерфейсов › предусловия,

    постусловия, инварианты Способы создания контрактов › на уровне абстракций (протоколов) - система типов › на уровне реализаций (классов) - тесты Контрактное программирование 61
  62. Это, по сути, формальное определение Observable › подписчику может быть

    отправлено ноль или более .next событий › отправив .error или .completed событие, последовательность завершается и не может больше производить других элементов Гайдлайны. Контракт Observable 62 Валидные стримы: Невалидные стримы:
  63. В реальности нужно больше гарантий › Observable - слишком общая

    абстракция › по интерфейсу нельзя понять «горячий» стрим или «холодный» › вынуждены описывать реализацию в комментариях к протоколам или использовать специализированные стримы (Traits*) › для разных реализаций стрима нужна разная обработка (цепочка операторов) Проблема контракта в Rx 63 * - Driver, Single, Completable, Signal
  64. Определяется на основе конвенции и системы типов Контракт API protocol

    CountriesRepository { func fetchCountries(@escaping completion: ([Country]) -> Void) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) repo.fetchCountries { [weak self] in self?.update(withNewCountries: $0) }) } единственное значение без ошибки
  65. Пробуем сделать реактивным Идеальный мир protocol CountriesRepository { func fetchCountries()

    -> Observable<[Country]> } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) repo.fetchCountries().subscribe(onNext: { [weak self] in self?.update(withNewCountries: $0) }) } возможна ошибка не гарантировано одно значение
  66. Описываем контракт и надеемся, что он будет удовлетворен Реальность. Комментарии

    protocol CountriesRepository { // Нет ошибок // Единственное значение func fetchCountries() -> Observable<[Country]> }
  67. Используем Trait для «усиления» контракта, но от комментариев не уйти

    Реальность. Trait + комментарии protocol CountriesRepository { // Нет ошибок func fetchCountries() -> Single<[Country]> }
  68. Новое требование - отображать кол-во стран в плейсхолдере Идеальный мир

    override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let countriesStream = repo.fetchCountries() countriesStream.subscribe(onNext: { ... }) countriesStream.map { "\($0.count) countries" } .subscribe(onNext: { placeholder in self.searchBar.placeholder = placeholder }) } подписка 1 подписка 2
  69. Реальность. Cold Observable 69 Ни комментарии, ни система типов не

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

    реализацию override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let countriesStream = repo.fetchCountries().publish() countriesStream.subscribe(onNext: { ... }) countriesStream.map { "\($0) countries" } .subscribe(onNext: { ... }) countriesStream.connect() }
  71. Контрактов и гайдлайнов не всегда достаточно Проблема 71 Приходится выбирать

    - лишний код или комментарии Снижение читаемости кода или повышение вероятности ошибки Повышается сложность и стоимость рефакторинга
  72. › Определяйте контракты на уровне протоколов с помощью Traits, а

    на уровне реализаций - тестов › Не используйте Rx там, где его контрактов недостаточно или нужны более жесткие гарантии Рекомендации 72
  73. Непониманием устройства используемого фреймворка › особенности реализации примитивов RxSwift ›

    Scheduler или как применять observeOn, subscribeOn › переход в парадигму Rx и обратно › создание оператора, которого нет в Rx › когда нужно «шарить» стримы › где использовать Traits вместо простого Observable › и множество других… Проблемы вызванные 73
  74. Непониманием устройства используемого фреймворка › особенности реализации примитивов RxSwift ›

    Scheduler или как применять observeOn, subscribeOn › переход в парадигму Rx и обратно › создание оператора, которого нет в Rx › когда нужно «шарить» стримы › где использовать Traits вместо простого Observable › и множество других… Проблемы вызванные 74
  75. Устройство фреймворка

  76. Подписчики › нужно ли ретейнить самостоятельно Операторы и Subjects ›

    когда создаются › кто отвечает за освобождение ресурсов Подписки › нужно ли сохранять › вызывать dispose самому или использовать DisposeBag Особенности реализации примитивов RxSwift 76
  77. Базовый принцип 77 Observable Observer create Observable subscribe Disposable create

    Observer 1 2 dispose
  78. RxSwift освобождает ресурсы, предотвращая утечки памяти

  79. › Ресурсы операторов освобождаются при получении терминальных событиях или уничтожении

    подписок › При совпадении времени жизни объекта и подписки удобно использовать DisposeBag Выводы 79
  80. Особености Subject

  81. BehaviorSubject › подписчикам не форвардятся события, если Subject получал .error

    или .completed до момента подписки › подписчик ретейнится до вызова dispose у самого сабджекта или подписки, полученной при вызове subscribe PublishSubject › аналогично за исключением того, что ссылки на подписчиков удаляются при получении терминального события Особенности Subject 81
  82. Особенность BehaviorSubject let subject = BehaviorSubject<Bool>(value: false) subject.onCompleted() _ =

    subject.subscribe(PrintObserver<Bool>()) subject.onNext(true) subject.onNext(false) PrintObserver<Bool> did receive: completed PrintObserver<Bool> deallocated Переходит в состояние Disposed. Перестает «прокидывать»
  83. А зачем мне вообще об этом думать и знать?

  84. Сломанные кнопки 84

  85. Подписка ControlEvent на PublishSubject Пример из жизни class WayPointViewModel {

    ... var taps = PublishSubject<Void>() } class WayPointCellView: GenericBindableView<WayPointViewModel> { override func bind(to viewModel: WayPointViewModel) { ... clearButton.rx.tap.bind(to: viewModel.taps).disposed(by: binding) } }
  86. Используем statefull вью- модели в MVVM Проблема 86 Время жизни

    вью оказывалось меньше чем у вью-модели PublishSubject получал .completed от ControlEvent при deinit вью
  87. › Читайте документацию для типов, которые используете › Обновляйте RxSwift.

    Переходите на Relay вместо Subject › Используйте Observable.create вместо Relay, Subject где возможно Рекомендации 87
  88. Schedulers

  89. Scheduler абстрагирует способ выполнения работы From RxSwift documentation

  90. По-умолчанию код RxSwift однопоточный › используется CurrentThreadScheduler › код подписок

    и операторов выполняется на потоке, на котором выполнен subscribe Управление выполняется за счет › использования observeOn, subscribeOn › передачи scheduler некоторым операторам Scheduler. Принципы 90
  91. subscribeOn › указываем scheduler, на котором будет выполнена подписка Observer’а,

    идущего ниже по цепочке на Observable идущий выше по цепочке observerOn › указывает скедулер, на котором будет вызван on(event:) у Observer’а, идущего ниже по цепочке Управление 91
  92. Поведение по-умолчанию func just(n: Int) -> Observable<Int> { return Observable<Int>.create

    { observer -> Disposable in log("subscribed") observer.onNext(3)) observer.onCompleted() return Disposables.create() } } just(3).subscribe(onNext: { (num) in log("onNext: \(num)”) }) [12:51:42]com.apple.main-thread subscribed [12:51:45]com.apple.main-thread onNext: 3
  93. Порядок исполнения 93 Создание JustObservable Вызов JustObservable.subscribe Вызов onNext main-thread

  94. subscribeOn just(n: 10) .subscribeOn(DispatchQueueScheduler(.background)) .subscribe(onNext: { (num) in log("onNext: \(num)”)

    }) [8:01:03]thread: <NSThread: 0x600000274580> subscribed [8:01:03]thread: <NSThread: 0x600000274580> onNext: 10 «Прорастает» вверх по цепочке операторов
  95. Порядок исполнения при subscribeOn 95 Вызов SubscribeOnObservable.subscribe Создание JustObservable Создание

    SubscribeOnObservable SubscribeOnSink.onNext Вызов onNext JustObservable.subscribe(SubscribeOnSink) скедулинг background queue main-thread
  96. observeOn just(n: 10) .subscribeOn(DispatchQueueScheduler(.background)) .observeOn(MainScheduler.instance) .subscribe(onNext: { (num) in log("onNext:

    \(num)”) }) [8:01:03]thread: <NSThread: 0x600000274580> subscribed [8:01:03]com.apple.main-thread onNext: 10 «Действует» вниз по цепочке операторов до следующего observeOn
  97. Рекомендации 97 › Не «зашивайте» логику скедулинга на верхних уровнях

    приложения (сервисы) › Старайтесь держать subscribeOn и observeOn рядом и как можно ближе к Presentation слою › Применяйте скедулинг когда это действительно нужно или для тестов
  98. Перевод в Rx парадигму 98 Асинхронные методы с completion-блоками ›

    Observable.fromAsync «Listeners API» › Observable.create + Listener, который форвардит подписчику нотификации Расширения для системного и собственного API › extension Reactive<Base> where Base: ...
  99. Можно взять отсюда или подключить RxSwiftExt Асинхронные методы с completion-блоками

    extension Observable { static func fromAsync(_ asyncRequest: @escaping (@escaping (Element) -> Void) -> Void) -> Observable<Element> { return Observable.create({ (o) -> Disposable in asyncRequest({ (result) in o.onNext(result) o.onCompleted() }) return Disposables.create() }) } }
  100. Готовые «переходы» в Rx парадигму 100 UIButton › tap UIControl

    › isEnabled, isSelected NotificationCenter › notification(name:) и многие другие…
  101. Расширяем интерфейс существующих компонентов, делая его Реактивное API с помощью

    RxSwift extension Reactive where Base: TimeInfoView { public var date: Binder<Date> { return Binder(self.base) { element, value in element.date = value } } } Observable<Int> .interval(1.0) .map { _ in Date() } .bind(to: timeInfoView.rx.date) .disposed(by: bag)
  102. Расширяем интерфейс существующих компонентов, делая его Реактивное API с помощью

    RxSwift extension Reactive where Base: TimeInfoView { public var date: Binder<Date> { return Binder(self.base) { element, value in element.date = value } } } Observable<Int> .interval(1.0) .map { _ in Date() } .bind(to: timeInfoView.rx.date) .disposed(by: bag)
  103. › Переиспользуйте компоненты, а не инфраструктуру/архитектуру › Перед написанием extensions

    к типам из Cocoa проверьте, возможно они там уже есть › Повторяйте поведение, реализованное в библиотеке Рекомендации 103
  104. Есть ли смысл? Да Отличный инструмент для решения ограниченного ряда

    задач › работа с потоками данных и манипуляциями над ними › отложенное выполнение задач › логика сильно завязана на время Обладает рядом преимуществ › декларативный, тестируемый, поддерживаемый код › хорошая абстракция для concurrency 104
  105. Есть ли смысл? Нет Может усложнить работу › скрывает синхронное

    выполнение за абстракцией потока › невозможно на уровне API представить поток без ошибок › код сложнее отлаживать › начинает «прорастать» в код, который можно написать проще Привносит ряд ограничений › увеличивается размер приложения 105
  106. 106

  107. Полезные ссылки. Спасибо! trimonovds@yandex-team.ru t.me/trimonovds 107