Flux with RxSwift
AbemaTV Developer Conference 2016
Yuji Hato
About me
Yuji HatoCyberAgent, Inc. / AbemaTV, Inc.
dekatotoro
@dekatotoro
Contributed services
What is Flux?
What is Flux?
https://facebook.github.io/flux/docs/overview.html
“Data in a Flux application flows in a single direction”
Why Flux?
Why Flux?
昨今のアプリ開発は複雑化の一途を辿っており状態管理が大変
Why Flux?AbemaTVの状態管理 :
Cast
オンデマンド課金
CM
Filler
画質
視聴予約 etc…
番組 Fresh
コメント視聴数
FullScreen
Feed
Why Flux?
つらい…
Why Flux?
UI 操作・時間に伴う複雑な状態遷移を分かりやすくしたい
Why Flux?
View 間の依存関係を減らしたい
MVVMMVC
DDD
Flux
Clean ArchitectureMVP
Why Flux?
Flux が向いてそう… ?
Flux with RxSwift
Flux with RxSwift
Flux with RxSwift
Event Stream で繋いじゃおうぜ
Flux with RxSwift
Flux with RxSwift
Eventは2種類の Hot Observableを使う
Flux with RxSwift
PublishSubject
一切キャッシュしない Subject
Flux with RxSwift
Variable
直近の値を1つだけキャッシュする Subject※BehaviorSubjectのwrapper
Dispatcher
Dispatcher
DispatcherDispatcherのフロー
DispatcherActionが dispatchする
Dispatcherdispatchされたら Storeへ通知
Dispatcher
class DispatchSubject<Element>: ObservableType, ObserverType { typealias E = Element fileprivate let subject = PublishSubject<E>() func dispatch(_ value: E) { on(.next(value)) } func on(_ event: Event<E>) { subject.on(event) }
func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.E == E { return subject.subscribe(observer) }}
Dispatcher用の DispatchSubject
Dispatcher
class DispatchSubject<Element>: ObservableType, ObserverType { typealias E = Element fileprivate let subject = PublishSubject<E>() func dispatch(_ value: E) { on(.next(value)) } func on(_ event: Event<E>) { subject.on(event) }
func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.E == E { return subject.subscribe(observer) }}
PublishSubjectのWrapper
Dispatcher
class DispatchSubject<Element>: ObservableType, ObserverType { typealias E = Element fileprivate let subject = PublishSubject<E>() func dispatch(_ value: E) { on(.next(value)) } func on(_ event: Event<E>) { subject.on(event) }
func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.E == E { return subject.subscribe(observer) }}
dispatchすると eventを発行します
Dispatcher
class SomeDispatcher { static let shared = SomeDispatcher() let loading = DispatchSubject<Bool>() let error = DispatchSubject<Error>() let someModel = DispatchSubject<SomeModel>()
…}
Dispatcherクラス
Dispatcher
class SomeDispatcher { static let shared = SomeDispatcher() let loading = DispatchSubject<Bool>() let error = DispatchSubject<Error>() let someModel = DispatchSubject<SomeModel>()
…}
ActionTypeの代わりに DispatchSubjectを複数用意※Dispatcherクラスも用途ごとに分けてます
Dispatcher
func someAction(value: Bool) { … dispatcher.loading.dispatch(value) … }
Actionが dispatch
Action
Action
ActionActionのフロー
ActionViewから Actionを実行
Action必要なデータを取得する
ActionUI, Web, DB, Devicesを外部 IFとして捉える
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
Actionデータの取得後 Dispatcherへ流す
Action
func someAction(query: String) { dispatcher.loading.dispatch(true)
API.getSome(with: query) .do(onError: { [unowned self] error in self.dispatcher.error.dispatch(error) self.dispatcher.loading.dispatch(false) }) .do(onCompleted: { [unowned self] error in self.dispatcher.loading.dispatch(false) }) .subscribe(onNext: { [unowned self] response in let someModel = SomeModel.make(from: response) self.dispatcher.someModel.dispatch(someModel) }) .addDisposableTo(disposeBag) }
APIを実行する例
Action
func someAction(query: String) { dispatcher.loading.dispatch(true)
API.getSome(with: query) .do(onError: { [unowned self] error in self.dispatcher.error.dispatch(error) self.dispatcher.loading.dispatch(false) }) .do(onCompleted: { [unowned self] error in self.dispatcher.loading.dispatch(false) }) .subscribe(onNext: { [unowned self] response in let someModel = SomeModel.make(from: response) self.dispatcher.someModel.dispatch(someModel) }) .addDisposableTo(disposeBag) }
ローディングして API実行
Action
func someAction(query: String) { dispatcher.loading.dispatch(true)
API.getSome(with: query) .do(onError: { [unowned self] error in self.dispatcher.error.dispatch(error) self.dispatcher.loading.dispatch(false) }) .do(onCompleted: { [unowned self] error in self.dispatcher.loading.dispatch(false) }) .subscribe(onNext: { [unowned self] response in let someModel = SomeModel.make(from: response) self.dispatcher.someModel.dispatch(someModel) }) .addDisposableTo(disposeBag) }
エラーの時
Action
func someAction(query: String) { dispatcher.loading.dispatch(true)
API.getSome(with: query) .do(onError: { [unowned self] error in self.dispatcher.error.dispatch(error) self.dispatcher.loading.dispatch(false) }) .do(onCompleted: { [unowned self] error in self.dispatcher.loading.dispatch(false) }) .subscribe(onNext: { [unowned self] response in let someModel = SomeModel.make(from: response) self.dispatcher.someModel.dispatch(someModel) }) .addDisposableTo(disposeBag) }
レスポンスが返ってきた時
Action
func someAction(query: String) { dispatcher.loading.dispatch(true)
API.getSome(with: query) .do(onError: { [unowned self] error in self.dispatcher.error.dispatch(error) self.dispatcher.loading.dispatch(false) }) .do(onCompleted: { [unowned self] error in self.dispatcher.loading.dispatch(false) }) .subscribe(onNext: { [unowned self] response in let someModel = SomeModel.make(from: response) self.dispatcher.someModel.dispatch(someModel) }) .addDisposableTo(disposeBag) }
実行が完了した時
Store
Store
StoreStoreのフロー
StoreDispatcherの eventを Observeする
StoreDispatcherのイベントが流れてくる
StoreDispatcherのイベントを検知したらデータを更新
Storeデータを更新したら Viewへ通知
Flux: Store
class Store { let disposeBag = DisposeBag() func bind<O, E>(_ observable: O, _ param: Variable<E>) where O: ObservableType, E == O.E { observable.bindTo(param).addDisposableTo(disposeBag) }
func bind<O, E>(_ observable: O, _ param: PublishSubject<E>) where O: ObservableType, E == O.E { observable.bindTo(param).addDisposableTo(disposeBag) }}
Store親クラス
Flux: Store
class Store { let disposeBag = DisposeBag() func bind<O, E>(_ observable: O, _ param: Variable<E>) where O: ObservableType, E == O.E { observable.bindTo(param).addDisposableTo(disposeBag) }
func bind<O, E>(_ observable: O, _ param: PublishSubject<E>) where O: ObservableType, E == O.E { observable.bindTo(param).addDisposableTo(disposeBag) }}
Variable用の bind
Flux: Store
class Store { let disposeBag = DisposeBag() func bind<O, E>(_ observable: O, _ param: Variable<E>) where O: ObservableType, E == O.E { observable.bindTo(param).addDisposableTo(disposeBag) }
func bind<O, E>(_ observable: O, _ param: PublishSubject<E>) where O: ObservableType, E == O.E { observable.bindTo(param).addDisposableTo(disposeBag) }}
PublishSubject用の bind
Flux: Store
class SomeStore: Store {
static let shared = SomeStore() let loading = Variable<Bool>(false) let error = PublishSubject<Error>() let someModel = Variable<SomeModel>(SomeModel())
init(dispatcher: SomeDispatcher = .shared) { super.init() bind(dispatcher.loading, loading) bind(dispatcher.error, error) bind(dispatcher.someModel, someModel) }}
Storeクラス
Flux: Store
class SomeStore: Store {
static let shared = SomeStore() let loading = Variable<Bool>(false) let error = PublishSubject<Error>() let someModel = Variable<SomeModel>(SomeModel()) init(dispatcher: SomeDispatcher = .shared) { super.init() bind(dispatcher.loading, loading) bind(dispatcher.error, error) bind(dispatcher.someModel, someModel) }}
Storeクラスは Singleton
Flux: Store
class SomeStore: Store {
static let shared = SomeStore() let loading = Variable<Bool>(false) let error = PublishSubject<Error>() let someModel = Variable<SomeModel>(SomeModel())
init(dispatcher: SomeDispatcher = .shared) { super.init() bind(dispatcher.loading, loading) bind(dispatcher.error, error) bind(dispatcher.someModel, someModel) }}
Storeの propertyは Variableと PublishSubject
Flux: Store
class SomeStore: Store {
static let shared = SomeStore() let loading = Variable<Bool>(false) let error = PublishSubject<Error>() let someModel = Variable<SomeModel>(SomeModel()) init(dispatcher: SomeDispatcher = .shared) { super.init() bind(dispatcher.loading, loading) bind(dispatcher.error, error) bind(dispatcher.someModel, someModel) }}
initで dispatcherと bind
View
View
ViewViewのフロー
ViewStoreと UIの eventを Observeする
ViewStoreのイベントが流れてくる
ViewStoreのイベントを検知したら Viewを更新
ViewUIの eventをトリガーに Actionを実行
View store.loading.asObservable() .distinctUntilChanged() .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] loading in self.loadingView.hidden(!loading) }) .addDisposableTo(rx_disposeBag) store.error .subscribe(onNext: { error in ErrorAction.show(.apiError(error)) }) .addDisposableTo(rx_disposeBag)
store.someModel.asObservable() .map { $0.dataSource } .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] dataSource in self.dataSource.value = dataSource self.tableView?.reloadData() }) .addDisposableTo(rx_disposeBag)
Storeの observe
View store.loading.asObservable() .distinctUntilChanged() .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] loading in self.loadingView.hidden(!loading) }) .addDisposableTo(rx_disposeBag) store.error .subscribe(onNext: { error in ErrorAction.show(.apiError(error)) }) .addDisposableTo(rx_disposeBag)
store.someModel.asObservable() .map { $0.dataSource } .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] dataSource in self.dataSource.value = dataSource self.tableView?.reloadData() }) .addDisposableTo(rx_disposeBag)
Storeの loadingを observe
View store.loading.asObservable() .distinctUntilChanged() .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] loading in self.loadingView.hidden(!loading) }) .addDisposableTo(rx_disposeBag) store.error .subscribe(onNext: { error in ErrorAction.show(.apiError(error)) }) .addDisposableTo(rx_disposeBag)
store.someModel.asObservable() .map { $0.dataSource } .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] dataSource in self.dataSource.value = dataSource self.tableView?.reloadData() }) .addDisposableTo(rx_disposeBag)
loadingViewの表示 /非表示
View store.loading.asObservable() .distinctUntilChanged() .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] loading in self.loadingView.hidden(!loading) }) .addDisposableTo(rx_disposeBag) store.error .subscribe(onNext: { error in ErrorAction.show(.apiError(error)) }) .addDisposableTo(rx_disposeBag)
store.someModel.asObservable() .map { $0.dataSource } .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] dataSource in self.dataSource.value = dataSource self.tableView?.reloadData() }) .addDisposableTo(rx_disposeBag)
Storeの errorを observe
View store.loading.asObservable() .distinctUntilChanged() .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] loading in self.loadingView.hidden(!loading) }) .addDisposableTo(rx_disposeBag) store.error .subscribe(onNext: { error in ErrorAction.show(.apiError(error)) }) .addDisposableTo(rx_disposeBag)
store.someModel.asObservable() .map { $0.dataSource } .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] dataSource in self.dataSource.value = dataSource self.tableView?.reloadData() }) .addDisposableTo(rx_disposeBag)
ErrorActionを実行
View store.loading.asObservable() .distinctUntilChanged() .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] loading in self.loadingView.hidden(!loading) }) .addDisposableTo(rx_disposeBag) store.error .subscribe(onNext: { error in ErrorAction.show(.apiError(error)) }) .addDisposableTo(rx_disposeBag)
store.someModel.asObservable() .map { $0.dataSource } .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] dataSource in self.dataSource.value = dataSource self.tableView?.reloadData() }) .addDisposableTo(rx_disposeBag)
Storeの someModelを observe
View store.loading.asObservable() .distinctUntilChanged() .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] loading in self.loadingView.hidden(!loading) }) .addDisposableTo(rx_disposeBag) store.error .subscribe(onNext: { error in ErrorAction.show(.apiError(error)) }) .addDisposableTo(rx_disposeBag)
store.someModel.asObservable() .map { $0.dataSource } .observeOn(MainScheduler.instance) .subscribe(onNext: { [unowned self] dataSource in self.dataSource.value = dataSource self.tableView?.reloadData() }) .addDisposableTo(rx_disposeBag)
tableViewを reload
View
searchBar.rx.text.asDriver() .throttle(0.3) .distinctUntilChanged() .drive(onNext: { query in SomeAction.someAction(query: query) }) .addDisposableTo(rx_disposeBag)
UIの eventを observe
View
searchBar.rx.text.asDriver() .throttle(0.3) .distinctUntilChanged() .drive(onNext: { query in SomeAction.someAction(query: query) }) .addDisposableTo(rx_disposeBag)
searchBarの text入力を observe
View
searchBar.rx.text.asDriver() .throttle(0.3) .distinctUntilChanged() .drive(onNext: { query in SomeAction.someAction(query: query) }) .addDisposableTo(rx_disposeBag)
入力された文字に変更がある毎に Actionを実行
Flux with RxSwift
一周しました!
Flux with RxSwift
Proposals
Proposals
• Dispatcherは一つにしたい• Storeは Singletonでなく、適切なライフサイクルにしたい• Storeを read-onlyにしたい
Conclusion
ConclusionPros
• 複数の ViewController, Viewを使って複雑な状態管理をするアプリケーションに向いている
• 開発者の実装が統一されやすい• View間の依存関係が減る
ConclusionCons
• Singletonで集中管理なので、好き嫌いあるかも
• 慣れるまで少しかかる• 実装が冗長に感じる… ?
Flux は枠組みなので、より良くするためにチームで Trial&Error してます
Flux with RxSwift
Thank you
参考資料https://facebook.github.io/flux/https://github.com/facebook/fluxhttps://github.com/thoughtbot/Deltahttps://github.com/yonekawa/SwiftFluxhttps://speakerdeck.com/ogaclejapan/flux-de-relax
https://github.com/dekatotoro/FluxWithRxSwiftSample
Flux with RxSwift Sample