$30 off During Our Annual Pro Sale. View Details »

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

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

yohei sugigami

January 25, 2018
Tweet

More Decks by yohei sugigami

Other Decks in Technology

Transcript

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

    View Slide

  2. Profile
    — Yohei Sugigami
    — susieyy
    — Twitter / Qiita / Github
    — New App Development
    Specialization
    — Clients
    — Folio.inc
    — New app developer
    — Wantedly.inc
    — Technical advisor

    View Slide

  3. ɹ
    ɹ
    Reduxͱ͸
    Reduxͷ3ͭͷݪଇ(Redux Three Principles) ͱ
    ୯ํ޲ͷσʔλϑϩʔ(Unidirectional Data Flow)

    View Slide

  4. Single source of truth
    ɹ
    State is read-only
    ɹ
    Mutations are wri!en
    as pure functions

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  9. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. ReduxΛͳʹͰ࣮૷͢Δ͔
    — ຊՈReduxͷεςοϓ਺͸318ͱඇৗʹগͳ͍
    — ࣮૷Λࢀߟʹࣗ෼ͰϙʔςΟϯά͢Δ͜ͱ΋Մೳͳ෼ྔ
    — ϥΠϒϥϦ΋͍͔࣮ͭ͘૷͞Ε͍ͯΔͷͰݕ౼ͯ͠ΈΔ

    View Slide

  14. ReduxܥϥΠϒϥϦ
    — ReSwift ˒4,247
    — ReactiveReSwift ˒71
    — KATANA ˒ 1,602
    — ReduxKit ˒583
    — Reactor ˒129

    View Slide

  15. FluxܥϥΠϒϥϦ
    — Dispatch ˒239
    — SwiftFlux ˒212
    — FluxWithRxSwiftSample ˒109
    — FluxxKit ˒33

    View Slide

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

    View Slide

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

    View Slide

  18. State
    Action
    Reducer
    Dispatch
    ActionCreator

    View Slide

  19. View Slide

  20. State
    protocol StateType { }

    View Slide

  21. State / e.g.
    struct AppState: ReSwift.StateType {
    var timelineState = TimelineState()
    var userProfileState = UserProfileState()
    }
    struct TimelineState: ReSwift.StateType {
    var tweets: [Tweet]
    var response: [Tweet]
    }

    View Slide

  22. State
    — Structͷ໦ߏ଄
    — ֤Struct͸ReSwi!.StateTypeϓϩτίϧʹ४ڌ͢Δ
    — ׳ྫతʹҰ൪্૚ͷঢ়ଶΛද͢StructΛAppStateͱ͢Δ

    View Slide

  23. View Slide

  24. Action
    protocol Action { }

    View Slide

  25. Action / e.g.
    extension TimelineState {
    enum Action: ReSwift.Action {
    case requestSuccess(response: [Tweet])
    case requestState(fetching: Bool)
    case requestError(error: Error)
    }
    }

    View Slide

  26. Action
    — ReSwi!.Actionϓϩτίϧʹ४ڌ͍ͯ͠Ε͹ɺStructͰ
    ΋ɺEnumͰ΋Α͍
    — ॲཧΛ༗͠ͳ͍ͨͩͷσʔλ
    — ReducerͰͲ͏͍͏ॲཧΛ͍͔ͨ͠ͷछྨͱͦͷΠϯϓο
    τσʔλʹͳΔ

    View Slide

  27. View Slide

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

    View Slide

  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
    }
    }

    View Slide

  30. Reducer
    — ReduxͰ͸ঢ়ଶมߋͰ͖Δͷ͸Reducer͚ͩͱ͍͏੍໿
    — Reducer͸ (state, action) => state Λຬͨ͢ঢ়ଶΛ࣋ͨ
    ͳ͍;ͭ͏ͷؔ਺ʢ७ਮؔ਺ʣͰͳ͚Ε͹ͳΒͳ͍

    View Slide

  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(
    reducer: appReduce,
    state: nil,
    middleware: [])

    View Slide

  32. Reducer / Initialization
    — ׳ྫతʹҰ൪্૚ͷঢ়ଶΛද͢StructΛappReduceͱ͢Δ
    — appReduceΛى఺ʹɺԼҐͷReducerʹActionΛϒϩʔυ
    Ωϟετ͢Δ

    View Slide

  33. View Slide

  34. Store
    open class Store: ReSwift.StoreType {
    var state: State! { get }
    private var reducer: Reducer
    open func dispatch(_ action: Action) {
    ...
    }
    open func subscribe(_ subscriber: S) {
    ...
    }
    open func unsubscribe(_ subscriber: AnyStoreSubscriber) {
    ...
    }
    ...
    }

    View Slide

  35. Store
    — StateͱReducerΛอ࣋͢Δγϯάϧτϯ
    — ActionͷσΟεύονϝιουΛ༗͢Δ
    — StateͷαϒεΫϥΠϒϝιουΛ༗͢Δ

    View Slide

  36. View Slide

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

    View Slide

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

    View Slide

  39. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  44. ෭࡞༻ʢඇಉظ௨৴ʣ/ Asynchronous Operations
    — ActionCreatorؔ਺͕७ਮؔ਺Ͱ͸ͳ͘ͳΔ
    — ؔ਺಺ʹ෭࡞༻ʢඇಉظ௨৴ʣ͕͋Δͱςετ͕͠ʹ͍͘
    — ඒ͘͠ͳ͍(ݸਓͷײ૝Ͱ͢)
    — ͱ͸͍͑ɺReducer͸७ਮؔ਺Ͱ෭࡞༻Λڐ༰͠ͳ͍ͷ
    ͰɺReducerʹ΋هड़Ͱ͖ͳ͍

    View Slide

  45. ɹ
    ɹ
    Redux(JS)Ͱ͸
    MiddlewareͰ෭࡞༻(ඇಉظॲཧ)Λѻ͏

    View Slide

  46. View Slide

  47. Middleware
    public typealias DispatchFunction = (Action) -> Void
    public typealias Middleware = (
    @escaping DispatchFunction,
    @escaping () -> State?
    ) -> (@escaping DispatchFunction) -> DispatchFunction

    View Slide

  48. Middleware / e.g.
    let loggingMiddleware: ReSwift.Middleware = { dispatch, getState in
    return { next in
    return { action in
    logger.info("! [Action] \(action)")
    return next(action)
    }
    }
    var appStore = Store(
    reducer: appReduce,
    state: nil,
    middleware: [loggingMiddleware]
    )

    View Slide

  49. ɹ
    ɹ
    Redux(JS)ͷ
    ඇಉظ༻Middlewareͨͪ

    View Slide

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

    View Slide

  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;

    View Slide

  52. ɹ
    ɹ
    ;Ή;Ή!

    View Slide

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

    View Slide

  54. rxThunkMiddleware
    struct SingleAction: ReSwift.Action {
    public let single: Single
    public let disposeBag: DisposeBag
    }
    let rxThunkMiddleware: ReSwift.Middleware = { 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)
    }
    }
    }
    }

    View Slide

  55. rxThunkMiddleware / e.g.
    func requstAsyncCreator(maxID: String) -> Store.ActionCreator {
    return { (state: AppState, store: Store) 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.just(action)
    }
    return SingleAction(single: s, disposeBag: state.timelineState.requestDisposeBag)
    }
    }

    View Slide

  56. ɹ
    ɹ
    Testability

    View Slide

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

    View Slide

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

    View Slide

  59. View Slide

  60. ɹ
    ɹ
    DIෆཁ!
    Dependency Injection

    View Slide

  61. ɹ
    ɹ
    ViewDataBinding

    View Slide

  62. ɹ
    ɹ
    ReSwi!#subscribe

    View Slide

  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()
    }
    }

    View Slide

  64. ɹ
    ɹ
    RxSwi!
    ReSwi!.StoreΛRxSwi!.Variableʹม׵

    View Slide

  65. let rxReduxStore = RxReduxStore(store: appStore)
    public class RxReduxStore: StoreSubscriber where AppStateType: StateType {
    public lazy var stateObservable: Observable = {
    return self.stateVariable.asObservable().observeOn(MainScheduler.instance)
    .shareReplayLatestWhileConnected()
    }()
    public var state: AppStateType { return stateVariable.value }
    private let stateVariable: Variable
    private let store: Store
    public init(store: Store) {
    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) -> Action?) {
    store.dispatch(actionCreatorProvider)
    }
    }

    View Slide

  66. class TimeLineViewController: UIViewController {
    fileprivate let disposeBag = DisposeBag()
    fileprivate let rxReduxStore: RxReduxStore
    init(_ rxReduxStore: RxReduxStore) {
    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)
    }
    }

    View Slide

  67. ɹ
    ɹ
    Viewͷࠩ෼ߋ৽

    View Slide

  68. View Slide

  69. ɹ
    ɹ
    ReactͰ͸

    View Slide

  70. ɹ
    ɹ
    Virtual DOM

    View Slide

  71. View Slide

  72. ɹ
    ɹ
    iOSͰ͸

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  76. View Slide

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

    View Slide

  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: return TweetsSectionController(o)
    case let o as DiffableWrap<[RecommendUser]>: return RecommendUsersSectionController(o)
    }
    }
    }
    }

    View Slide

  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

    View Slide

  80. View Slide

  81. ɹ
    ɹ
    One more thing !

    View Slide

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

    View Slide

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

    View Slide

  84. ɹ
    ɹ
    DEMO

    View Slide

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

    View Slide