Pro Yearly is on sale from $80 to $50! »

ReduxRxを活用したアプリアーキテクチャ

 ReduxRxを活用したアプリアーキテクチャ

Acbf3391de0494432a92221ffe89f34e?s=128

yohei sugigami

January 25, 2018
Tweet

Transcript

  1. Redux+RxΛ׆༻ͨ͠ΞϓϦΞʔ ΩςΫνϟ ɹ CA.swi! #5 2018/01/25@גࣜձࣾαΠόʔΤʔδΣϯτ Yohei Suginami ( @susieyy

    )
  2. Profile — Yohei Sugigami — susieyy — Twitter / Qiita

    / Github — New App Development Specialization — Clients — Folio.inc — New app developer — Wantedly.inc — Technical advisor
  3. ɹ ɹ Reduxͱ͸ Reduxͷ3ͭͷݪଇ(Redux Three Principles) ͱ ୯ํ޲ͷσʔλϑϩʔ(Unidirectional Data Flow)

  4. Single source of truth ɹ State is read-only ɹ Mutations

    are wri!en as pure functions
  5. Single source of truth / ৴པͰ͖Δ།Ұͷঢ়ଶ ΞϓϦશମͷঢ়ଶΛ̍ͭͷΦϒδΣΫτπϦʔͰදݱ

  6. State is read-only / ঢ়ଶ͸Πϛϡʔλϒϧ มߋ͢Δʹ͸ඞͣΞΫγϣϯΛൃߦ͢Δ

  7. Mutations are wri!en as pure functions / ७ਮؔ਺ ঢ়ଶมߋ͸෭࡞༻͕ͳ͍७ਮؔ਺Λ༻͍ɺঢ়ଶΛ௚઀มߋͤ ͣɺ৽͍͠ঢ়ଶΛ࡞ͬͯฦ͢

  8. ɹ ɹ ୯ํ޲ͷσʔλϑϩʔ Unidirectional Data Flow

  9. None
  10. POINT Viewͱঢ়ଶͷ׬શͳΔ෼཭ ViewͷϩδοΫ͸ͦͷॠؒͷΞϓϦ಺ͷ͢΂ͯͷঢ়ଶ͕ҰҙͰ ͋Δ͜ͱΛอূ͞ΕΔ ʢ ͦͷॠؒͷঢ়ଶʹରͯ͠ඇಉظͷհ ೖ͕ͳ͍ ʣ ඇಉظ࣮ߦதͷதؒঢ়ଶ΋ҙࣝ͢Δඞཁ͕ͳ͍ɺγϯάϧϝΠ ϯεϨου͚ͩͰಈ࡞͍ͯ͠ΔΑ͏ͳִؒͰϩδοΫ͕ॻ͚Δ

  11. POINT Πϛϡʔλϒϧͱ७ਮؔ਺Λ׆༻ͯ͠పఈతʹ෭࡞༻ͷഉআ ʢ෼཭ʣʹప͍ͯ͠Δ͜ͱ͕ɺଞͷMVC,MVVM,CleanͳͲͷ ΞʔΩςΫνϟͱҟͳΔઃܭࢥ૝Ͱ͋ΓɺγϯϓϧͰݎ࿚Ͱ׌ ͭՄಡੑͷߴ͍ίʔσΟϯάΛՄೳͱ͢Δݯઘ Πϛϡʔλϒϧ͕ѻ͑ͯؔ਺ܕࢦ޲ͳSwiftͱ΋૬ੑ͕Α͍

  12. POINT ༧ଌՄೳͳܗͰίʔυΛએݴతʹߏ଄Խ͢Δ͜ͱ - ༧ଌՄೳ - ঢ়ଶΛҰݩతʹ؅ཧ - ঢ়ଶมԽ͸γʔέϯγϟϧ - ෭࡞༻ͱͷ෼཭(ඇಉظతมԽͷഉআ)

    - એݴత - ঢ়ଶมԽͷىҼ͕໌ࣔత ʢ ActionΛDispatch ʣ - ঢ়ଶมԽ͸७ਮؔ਺
  13. ReduxΛͳʹͰ࣮૷͢Δ͔ — ຊՈReduxͷεςοϓ਺͸318ͱඇৗʹগͳ͍ — ࣮૷Λࢀߟʹࣗ෼ͰϙʔςΟϯά͢Δ͜ͱ΋Մೳͳ෼ྔ — ϥΠϒϥϦ΋͍͔࣮ͭ͘૷͞Ε͍ͯΔͷͰݕ౼ͯ͠ΈΔ

  14. ReduxܥϥΠϒϥϦ — ReSwift ˒4,247 — ReactiveReSwift ˒71 — KATANA ˒

    1,602 — ReduxKit ˒583 — Reactor ˒129
  15. FluxܥϥΠϒϥϦ — Dispatch ˒239 — SwiftFlux ˒212 — FluxWithRxSwiftSample ˒109

    — FluxxKit ˒33
  16. ݕ౼݁Ռ — ReSwift͕Ұ൪༗໊ — ؔ਺΍ΫϩʔδϟʔΛத৺ʹઃܭ͞Ε͓ͯΓɺ࣮૷͕ͱͯ ΋៉ྷ — ϦϦʔεස౓΋΄ͲΑ͘ɺISSUE΋׆ൃ

  17. ɹ ɹ ReSwi!ͱ͸ ReSwi!ͷ࣮૷ͱར༻ྫΛݟͯΈΔ

  18. State Action Reducer Dispatch ActionCreator

  19. None
  20. State protocol StateType { }

  21. State / e.g. struct AppState: ReSwift.StateType { var timelineState =

    TimelineState() var userProfileState = UserProfileState() } struct TimelineState: ReSwift.StateType { var tweets: [Tweet] var response: [Tweet] }
  22. State — Structͷ໦ߏ଄ — ֤Struct͸ReSwi!.StateTypeϓϩτίϧʹ४ڌ͢Δ — ׳ྫతʹҰ൪্૚ͷঢ়ଶΛද͢StructΛAppStateͱ͢Δ

  23. None
  24. Action protocol Action { }

  25. Action / e.g. extension TimelineState { enum Action: ReSwift.Action {

    case requestSuccess(response: [Tweet]) case requestState(fetching: Bool) case requestError(error: Error) } }
  26. Action — ReSwi!.Actionϓϩτίϧʹ४ڌ͍ͯ͠Ε͹ɺStructͰ ΋ɺEnumͰ΋Α͍ — ॲཧΛ༗͠ͳ͍ͨͩͷσʔλ — ReducerͰͲ͏͍͏ॲཧΛ͍͔ͨ͠ͷछྨͱͦͷΠϯϓο τσʔλʹͳΔ

  27. None
  28. Reducer typealias Reducer<ReducerStateType> = (action: Action, state: ReducerStateType?) -> ReducerStateType

  29. Reducer / e.g extension TimelineState { public static func reducer(action:

    ReSwift.Action, state: TimelineState?) -> TimelineState { var state = state ?? TimelineState() guard let action = action as? TimelineState.Action else { return state } switch action { case let .requestState(fetching): state.fetching = fetching state.error = nil case let .requestSuccess(response): state.fetching = false state.response = response state.dataSourceElements = DataSourceElements(response.map({ DiffableWrap($0) })) case let .requestError(error): state.fetching = false state.error = error } return state } }
  30. Reducer — ReduxͰ͸ঢ়ଶมߋͰ͖Δͷ͸Reducer͚ͩͱ͍͏੍໿ — Reducer͸ (state, action) => state Λຬͨ͢ঢ়ଶΛ࣋ͨ

    ͳ͍;ͭ͏ͷؔ਺ʢ७ਮؔ਺ʣͰͳ͚Ε͹ͳΒͳ͍
  31. Reducer / Initialization func appReduce(action: ReSwift.Action, state: AppState?) -> AppState

    { var state = state ?? AppState() state.timelineState = TimelineState.reducer( action: action, state: state.timelineState) state.userProfile = UserProfileState.reducer( action: action, state: state.userProfile) return state } var appStore = ReSwift.Store<AppState>( reducer: appReduce, state: nil, middleware: [])
  32. Reducer / Initialization — ׳ྫతʹҰ൪্૚ͷঢ়ଶΛද͢StructΛappReduceͱ͢Δ — appReduceΛى఺ʹɺԼҐͷReducerʹActionΛϒϩʔυ Ωϟετ͢Δ

  33. None
  34. Store open class Store<State: ReSwift.StateType>: ReSwift.StoreType { var state: State!

    { get } private var reducer: Reducer<State> open func dispatch(_ action: Action) { ... } open func subscribe<S: StoreSubscriber>(_ subscriber: S) { ... } open func unsubscribe(_ subscriber: AnyStoreSubscriber) { ... } ... }
  35. Store — StateͱReducerΛอ࣋͢Δγϯάϧτϯ — ActionͷσΟεύονϝιουΛ༗͢Δ — StateͷαϒεΫϥΠϒϝιουΛ༗͢Δ

  36. None
  37. Dispatch / e.g. let action = TimelineState.Action.requestState(fetching: true) appStore.dispatch(action)

  38. Dispatch — ViewଆͰActionͷΠϯελϯεΛ࡞੒͠ɺStoreͷ DispatchϝιουΛίʔϧ͢Δ

  39. None
  40. ActionCreator typealias ActionCreator = (state: ReSwift.State, store: ReSwift.StoreType) -> ReSwift.Action?

  41. ActionCreator — DispatchՄೳͳؔ਺ͰActionΛฦ͢ — ActionCreatorؔ਺಺Ͱ͸StateʹΞΫηεՄೳͰɺState ΛՃ޻ͯ͠ActionΛ࡞੒͢Δ༻్ʹར༻Ͱ͖Δ — ActionCreatorؔ਺಺Ͱ͸StoreʹΞΫηεՄೳͰɺ DispatchϝιουΛίʔϧ͢Δ͜ͱ΋Ͱ͖Δ

  42. ɹ ɹ ReSwi! Rx׆༻ฤ ෭࡞༻ʢඇಉظ௨৴ʣ/ ViewDataBinding

  43. ෭࡞༻ʢඇಉظ௨৴ʣ/ Asynchronous Operations ReSwi!ͷREADMEʹΑΔඇಉظ௨৴ͷྫ ActionCreator಺Ͱ෭࡞༻ʢඇಉظ௨৴ʣΛߦ͍ͬͯΔ func fetchGitHubRepositories(state: State, store: Store<State>)

    -> Action? { guard case let .LoggedIn(configuration) = state.authenticationState.loggedInState else { return nil } Octokit(configuration).repositories { response in dispatch_async(dispatch_get_main_queue()) { store.dispatch(SetRepostories(repositories: response)) } } return nil }
  44. ෭࡞༻ʢඇಉظ௨৴ʣ/ Asynchronous Operations — ActionCreatorؔ਺͕७ਮؔ਺Ͱ͸ͳ͘ͳΔ — ؔ਺಺ʹ෭࡞༻ʢඇಉظ௨৴ʣ͕͋Δͱςετ͕͠ʹ͍͘ — ඒ͘͠ͳ͍(ݸਓͷײ૝Ͱ͢) —

    ͱ͸͍͑ɺReducer͸७ਮؔ਺Ͱ෭࡞༻Λڐ༰͠ͳ͍ͷ ͰɺReducerʹ΋هड़Ͱ͖ͳ͍
  45. ɹ ɹ Redux(JS)Ͱ͸ MiddlewareͰ෭࡞༻(ඇಉظॲཧ)Λѻ͏

  46. None
  47. Middleware public typealias DispatchFunction = (Action) -> Void public typealias

    Middleware<State> = ( @escaping DispatchFunction, @escaping () -> State? ) -> (@escaping DispatchFunction) -> DispatchFunction
  48. Middleware / e.g. let loggingMiddleware: ReSwift.Middleware<AppState> = { dispatch, getState

    in return { next in return { action in logger.info("! [Action] \(action)") return next(action) } } var appStore = Store<AppState>( reducer: appReduce, state: nil, middleware: [loggingMiddleware] )
  49. ɹ ɹ Redux(JS)ͷ ඇಉظ༻Middlewareͨͪ

  50. refs. Redux Middleware ɹ redux-thunk redux-sage redux-promise

  51. Redux thunkͷιʔείʔυʢશྔʣ function createThunkMiddleware(extraArgument) { return ({ dispatch, getState })

    => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
  52. ɹ ɹ ;Ή;Ή!

  53. ɹ ɹ RxSwi! ׆༻ͯ͠ඇಉظॲཧΛMiddlewareʹԡ͠ࠐΉ

  54. rxThunkMiddleware struct SingleAction: ReSwift.Action { public let single: Single<ReSwift.Action> public

    let disposeBag: DisposeBag } let rxThunkMiddleware: ReSwift.Middleware<AppState> = { dispatch, getState in return { next in return { action in if let action = action as? SingleAction { action.single .observeOn(MainScheduler.instance) .subscribe(onSuccess: { next($0) }) .disposed(by: action.disposeBag) } else { return next(action) } } } }
  55. rxThunkMiddleware / e.g. func requstAsyncCreator(maxID: String) -> Store<AppState>.ActionCreator { return

    { (state: AppState, store: Store<AppState>) in if state.timelineState.fetching { return nil } let s = TwitterManager.shared.timeline(maxID: maxID) .map { return Timeline.Action.requestSuccess(response: $0) } .catchError { let action = Timeline.Action.requestError(error: $0) return Single<ReSwift.Action>.just(action) } return SingleAction(single: s, disposeBag: state.timelineState.requestDisposeBag) } }
  56. ɹ ɹ Testability

  57. ɹ ɹ Reduxͷੈք͸Πϛϡʔλϒϧ ७ਮؔ਺ɺ෭࡞༻ͷΈͰߏங͞ΕΔ

  58. — Πϛϡʔλϒϧ͸ෆม — ActionʢΠϯϓοτύϥϝʔλʔʣɺStateʢσʔλʣ — ७ਮؔ਺͸ςετ͕༰қ — ೚ҙͷΠϯϓοτʹରͯ͠Ξ΢τϓοτ͕Ұҙʹܾ·Δ — ActionCreatorͱReducer

    — ෭࡞༻͸ςετ͕ෳࡶ — Middlewareͷ෦෼͸ςετ࣌ʹελϒʹࠩ͠ସ͑
  59. None
  60. ɹ ɹ DIෆཁ! Dependency Injection

  61. ɹ ɹ ViewDataBinding

  62. ɹ ɹ ReSwi!#subscribe

  63. override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) store.subscribe(self) { subcription in

    subcription.select { state in state.repositories } } } override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) store.unsubscribe(self) } func newState(state: Response<[Repository]>?) { if case let .Success(repositories) = state { dataSource?.array = repositories tableView.reloadData() } }
  64. ɹ ɹ RxSwi! ReSwi!.StoreΛRxSwi!.Variableʹม׵

  65. let rxReduxStore = RxReduxStore<AppState>(store: appStore) public class RxReduxStore<AppStateType>: StoreSubscriber where

    AppStateType: StateType { public lazy var stateObservable: Observable<AppStateType> = { return self.stateVariable.asObservable().observeOn(MainScheduler.instance) .shareReplayLatestWhileConnected() }() public var state: AppStateType { return stateVariable.value } private let stateVariable: Variable<AppStateType> private let store: Store<AppStateType> public init(store: Store<AppStateType>) { self.store = store self.stateVariable = Variable(store.state) self.store.subscribe(self) } deinit { self.store.unsubscribe(self) } public func newState(state: AppStateType) { self.stateVariable.value = state } public func dispatch(_ action: Action) { store.dispatch(action) } public func dispatch(_ actionCreatorProvider: @escaping (AppStateType, ReSwift.Store<AppStateType>) -> Action?) { store.dispatch(actionCreatorProvider) } }
  66. class TimeLineViewController: UIViewController { fileprivate let disposeBag = DisposeBag() fileprivate

    let rxReduxStore: RxReduxStore<AppState> init(_ rxReduxStore: RxReduxStore<AppState>) { self.rxReduxStore = rxReduxStore super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewDidLoad() rxReduxStore.stateObservable .map { $0.timelineState.dataSourceElements } .bind(to: adapter.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) rxReduxStore.stateObservable .map { $0.timelineState.fetching } .distinctUntilChanged() .bind(to: loadingView.rx.fetching) .disposed(by: disposeBag) } }
  67. ɹ ɹ Viewͷࠩ෼ߋ৽

  68. None
  69. ɹ ɹ ReactͰ͸

  70. ɹ ɹ Virtual DOM

  71. None
  72. ɹ ɹ iOSͰ͸

  73. ɹ ɹ IGListKit1 1 https://github.com/Instagram/IGListKit

  74. ɹ ɹ σʔλࠩ෼ΞϧΰϦζϜʹΑΔߴ଎ࠩ෼ߋ৽

  75. ɹ ɹ ίϯϙʔωϯτࢦ޲Ͱଟ༷ͳViewΛѻ͑Δ

  76. None
  77. — UICollectionViewΛϕʔεʹͰ͖͍ͯΔ — σʔλ͸͍ΖΜͳܕͷཁૉΛؚΉ̍࣍ݩ഑ྻ — σʔλͷཁૉͷܕͰίϯϙʔωϯτΛ෼ذ͢Δ — σʔλͷཁૉ͸ൺֱతՄೳͳͨΊʹIGListKitͷϓϩτίϧ ʹ४ڌ͢Δ(EquitableΈ͍ͨͳ΋ͷ) —

    ObjCͳͱ͜Ζ͕൵͍͠! — ϓϩτίϧ४ڌͷͨΊʹNSObjectΛܧঝ͕ඞཁ
  78. final class TimeLineViewController: UIViewController { fileprivate let dataSource = DataSource()

    fileprivate lazy var adapter: ListAdapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self) fileprivate let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.rx .setDataSource(dataSource) .disposed(by: disposeBag) rxReduxStore.stateObservable .map { $0.timelineState.dataSourceElements } .bind(to: adapter.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) } } extension TimeLineViewController { fileprivate final class DataSource: AdapterDataSource { override func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { switch object { case let o as DiffableWrap<Tweet>: return TweetsSectionController(o) case let o as DiffableWrap<[RecommendUser]>: return RecommendUsersSectionController(o) } } } }
  79. RxDatasource — https://github.com/RxSwiftCommunity/RxDataSources — O(N) algorithm for calculating differences —

    the algorithm has the assumption that all sections and items are unique so there is no ambiguity — in case there is ambiguity, fallbacks automagically on non animated refresh
  80. None
  81. ɹ ɹ One more thing !

  82. ɹ ɹ Redux͸Debug͕͠΍͍͢

  83. ɹ ɹ ReduxDevToolsͰঢ়ଶͷϞχλϦϯά

  84. ɹ ɹ DEMO

  85. Conclusion ɹ ɹ ReduxΞʔΩςΫνϟྑ͍Α!