Flux with RxSwift
AbemaTV Developer Conference 2016
Yuji Hato
About me
Yuji Hato
CyberAgent, 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”
What is Flux?
https://github.com/facebook/flux
What is Flux?
https://github.com/facebook/flux
What is Flux?
https://github.com/facebook/flux
Observer パターン !
Why Flux?
Why Flux?
昨今のアプリ開発は複雑化
の一途を辿っており状態管理
が大変
Why Flux?
AbemaTVの状態管理:
Cast
オンデマンド
課金
CM
Filler
画質
視聴予約
etc…
番組 Fresh
コメント
視聴数
FullScreen
Feed
Why Flux?
つらい…
Why Flux?
UI操作・時間に伴う複雑な
状態遷移を分かりやすくしたい
Why Flux?
View間の依存関係を減らしたい
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
Dispatcher
Dispatcherのフロー
Dispatcher
Actionがdispatchする
Dispatcher
dispatchされたら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
ObservableType, ObserverType
fileprivate let subject = PublishSubject<E>()
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
func dispatch(_ value: E) {
on(.next(value))
}
dispatchするとeventを発行します
Dispatcher
class SomeDispatcher {
static let shared = SomeDispatcher()
let loading = DispatchSubject<Bool>()
let error = DispatchSubject<Error>()
let someModel = DispatchSubject<SomeModel>()
…
}
Dispatcherクラス
Dispatcher
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
Action
Actionのフロー
Action
ViewからActionを実行
Action
必要なデータを取得する
Action
UI, 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
dispatcher.loading.dispatch(true)
API.getSome(with: query)
ローディングしてAPI実行
Action
.do(onError: { [unowned self] error in
self.dispatcher.error.dispatch(error)
self.dispatcher.loading.dispatch(false)
})
エラーの時
Action
.subscribe(onNext: { [unowned self] response in
let someModel = SomeModel.make(from: response)
self.dispatcher.someModel.dispatch(someModel)
})
レスポンスが返ってきた時
Action
.do(onCompleted: { [unowned self] error in
self.dispatcher.loading.dispatch(false)
})
実行が完了した時
Store
Store
Store
Storeのフロー
Store
DispatcherのeventをObserveする
Store
Dispatcherのイベントが流れてくる
Store
Dispatcherのイベントを検知したらデータを更新
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
func bind<O, E>(_ observable: O, _ param: Variable<E>) where O: ObservableType, E == O.E
{
observable.bindTo(param).addDisposableTo(disposeBag)
}
Variable用のbind
Flux: Store
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
static let shared = SomeStore()
StoreクラスはSingleton
Flux: Store
let loading = Variable<Bool>(false)
let error = PublishSubject<Error>()
let someModel = Variable<SomeModel>(SomeModel())
StoreのpropertyはVariableとPublishSubject
Flux: Store
init(dispatcher: SomeDispatcher = .shared) {
super.init()
bind(dispatcher.loading, loading)
bind(dispatcher.error, error)
bind(dispatcher.someModel, someModel)
}
initでdispatcherとbind
View
View
View
Viewのフロー
View
StoreとUIのeventをObserveする
View
Storeのイベントが流れてくる
View
Storeのイベントを検知したらViewを更新
View
UIの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
})
.addDisposableTo(rx_disposeBag)
Storeのloadingをobserve
View
self.loadingView.hidden(!loading)
loadingViewの表示/非表示
View
store.error
.subscribe(onNext: { error in
})
.addDisposableTo(rx_disposeBag)
Storeのerrorをobserve
View
ErrorAction.show(.apiError(error))
ErrorActionを実行
View
store.someModel.asObservable()
.map { $0.dataSource }
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [unowned self] dataSource in
})
.addDisposableTo(rx_disposeBag)
StoreのsomeModelをobserve
View
self.dataSource.value = dataSource
self.tableView?.reloadData()
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
})
.addDisposableTo(rx_disposeBag)
searchBarのtext入力をobserve
View
SomeAction.someAction(query: query)
})
入力された文字に変更がある毎にActionを実行
Flux with RxSwift
一周しました!
Flux with RxSwift
Proposals
Proposals
• Dispatcherは一つにしたい
• StoreはSingletonでなく、適切なライフサイク
ルにしたい
• Storeをread-onlyにしたい
Conclusion
Conclusion
Pros
• 複数のViewController, Viewを使って複雑な状態
管理をするアプリケーションに向いている
• 開発者の実装が統一されやすい
• View間の依存関係が減る
Conclusion
Cons
• Singletonで集中管理なので、好き嫌いあるかも
• 慣れるまで少しかかる
• 実装が冗長に感じる…?
Fluxは枠組みなので、より良くするために
チームでTrial&Errorしてます
Flux with RxSwift
Thank you
参考資料
https://facebook.github.io/flux/
https://github.com/facebook/flux
https://github.com/thoughtbot/Delta
https://github.com/yonekawa/SwiftFlux
https://speakerdeck.com/ogaclejapan/flux-de-relax
https://github.com/dekatotoro/FluxWithRxSwiftSample
Flux with RxSwift Sample

Flux with RxSwift

Editor's Notes

  • #3 まず、簡単に自己紹介します。 波戸と申します。ネット上ではだいたいdekatotoroという名前で活動しています。 AbemaTVのiOSアプリを開発してまして、 これまでは、AmebaアプリやAWAという音楽ストリーミングサービスのアプリなど開発してきました。
  • #5 アプリケーションの複雑さをなくすため、データの流れを一方向にします
  • #6 ・ViewのUser Interactionがイベントハンドラ(onClickなど)を実行します。 ・イベントハンドラの中でAction Creatorのを実行します。 ・Action CreatorはAPI呼び出しやDB操作を行ってデータを取得してDispatcherにactionを送信します。 ・Dispatcherはdispatchさせてcallback関数を登録します。 ・Storeは、callbackを呼び出して自信のデータを更新してイベントを発行します。 ・Viewはそのイベントを購読していて、StoreのデータがViewに反映されます。
  • #75 throttleはある期間はイベントを発行しないようにするオペレータです
  • #82 View階層が深い画面のdelegate地獄も無くなり Viewが分割しやすくなります。
  • #83 コード量は増える
  • #84 試行錯誤してます