RxSwift Demo -- 计算器(状态机)

233 阅读3分钟

在这个 计算器的 Demo 中,演示了状态机类型场景如何处理。

分为内部和外部,内部是指状态机,外部是指特殊的场景。

如果把状态机想象成一个黑盒的话,它跟外部的交互分为两种:

  1. 外部触发状态机改变状态,我们先管它叫反馈给状态机
  2. 外部监听状态机的状态,我们先管它叫订阅状态机

状态机

状态机需要知道:

  1. 初始的状态
  2. 新状态是如何根据当前状态和事件计算出来的:新状态 = 当前状态 + 事件
  3. 在什么线程执行新状态的获取过程
  4. UI 如何和状态交互

比如在计算器场景中,UI与状态机的交互有:

  1. 点击按钮改变状态机的状态
  2. 计算结果label 需要监听状态机的状态以展示计算结果

代码如下:

Observable.system(
        initialState: CalculatorState.initial,
        reduce: CalculatorState.reduce,
        scheduler: MainScheduler.instance,
        scheduledFeedback: uiFeedback
    )
    .subscribe()
    .disposed(by: disposeBag)

状态机内部是怎么实现的(内部实现嵌套层次多,导致代码不太好看,我只取重点代码):

let replaySubject = ReplaySubject<State>.create(bufferSize: 1)

let asyncScheduler = scheduler.async
            
let events: Observable<Event> = Observable.merge(scheduledFeedback.map { feedback in
    let state = ObservableSchedulerContext(source: replaySubject.asObservable(), scheduler: asyncScheduler)
        return feedback(state)
    })
    .observeOn(CurrentThreadScheduler.instance)

return events.scan(initialState, accumulator: reduce)
             .do(onNext: { output in
                replaySubject.onNext(output)
             }, onSubscribed: {
                replaySubject.onNext(initialState)
             })
             .subscribeOn(scheduler)
             .startWith(initialState)
             .observeOn(scheduler)

1. 首先是组合scheduledFeedback

scheduledFeedback是个闭包数组。由于外部需要定义 反馈给状态机 和 订阅状态机,所以该闭包需要提供以下参数:

  1. 以状态作为 Event 的源:订阅状态机需要的,也就是 ReplaySubject<State>
  2. scheduler:这个是我这篇博客的bug,我暂时还不知道为啥要提供这个,😆

scheduledFeedback 闭包会返回一个 以状态机的事件类型作为 Event 的源,在计算器的例子中也就是 Observable<CalculatorCommand>

events 是把外部定义的多个源组合而成的一个源。

2. 与外部定义的源交互

  1. 订阅外部定义的源们:events.scan(initialState, accumulator: reduce)
  2. 一旦状态发生改变,通过告知 replaySubject 来通知到外部:
.do(onNext: { output in
                replaySubject.onNext(output)
             }, onSubscribed: {
                replaySubject.onNext(initialState)
             })

外部如何定义源的

let uiFeedback: FeedbackLoop = bind(self) { this, state in
    let subscriptions = [
        state.map { $0.screen }.bind(to: this.resultLabel.rx.text),
        state.map { $0.sign }.bind(to: this.lastSignLabel.rx.text)
    ]

    let events: [Observable<CalculatorCommand>] = [
        this.allClearButton.rx.tap.map { _ in .clear },
        this.zeroButton.rx.tap.map { _ in .addNumber("0") },
        // 等等,没有列出来完
    ]
    return Bindings(subscriptions: subscriptions, events: events)
}
  1. subscriptions 是定义如何订阅状态机的
  2. events 是定义如何把按钮的点击事件转换成状态机需要的 CalculatorCommand 源

把 subscriptions 和 events 封装在 Bindings,我认为主要是为了 bind 方法方便。

下面看看 bind 方法里做了什么?

public func bind<State, Event>(_ bindings: @escaping (ObservableSchedulerContext<State>) -> (Bindings<Event>)) -> (ObservableSchedulerContext<State>) -> Observable<Event> {
    return { (state: ObservableSchedulerContext<State>) -> Observable<Event> in
        return Observable<Event>.using({ () -> Bindings<Event> in
            return bindings(state)
        }, observableFactory: { (bindings: Bindings<Event>) -> Observable<Event> in
            return Observable<Event>.merge(bindings.events)
                .enqueue(state.scheduler)
        })
    }
}

看起来好像只是把 一堆 events 转成了一个源,我不太确定,因为感觉跟方法名没关系😹

GitHubSignupViewController1 用这种方式怎么做

GitHubSignupViewController1 的需求:

  1. 输入名字 -> 通过网络验证名字的可用性 -> 可用性结果反馈在 label 上
  2. 输入密码 -> 本地验证密码的是否符合要求 -> 结果反馈在 label 上
  3. 再次输入密码 -> 本地验证再次输入的密码是否一致 -> 结果反馈在 label 上
  4. 以上3条都正确时 -> 注册按钮可用
  5. 点击注册 -> 发起网络请求,正在注册的状态,显示 loading
  6. 注册成功/失败 -> 弹窗提示

因为这个需求不像计算器有非常清晰的状态,所以没办法用状态机。

但是如果把 1、2、3 改成依赖关系:必须名字可用之后,才能输入密码,必须密码符合要求之后,才能输入再次密码。这样就有这样的状态: 初始 -> 输入名字 -> 输入密码 -> 再次输入密码 -> 准备好注册 -> 注册中 -> 注册成功/注册失败

这样就可以用状态机了。