RxExample GitHubSignup 部分代码解读

GitHubSignup 是一个注册例子的 Demo ,同时也是一个 MVVM 的 Demo 。但本节将重点介绍代码上为什么这样写,你可以从中了解到何时在代码中用 Rx 处理异步,如何合理的书写代码,以及如何优雅地处理网络请求状态。

事实上这个例子处理网络请求的方式是使用 using 操作符 hook 网络请求 Observable 的生命周期。

代码均在 RxExample 项目中,相关涉及文件如下:

  • GitHubSignup 文件夹所有内容
  • ActivityIndicator.swift

我们先来简单思考一下注册需要注意哪几个点,这里主要是表单验证问题:

  • 用户名不能重复,需要提交用户名到服务器验证
  • 注册密码有等级限制,比如长度、带大小写字母
  • 两次输入的密码相同

Protocols.swift 文件入手,这个文件有两个枚举 ValidationResultSignupState ,两个协议 GitHubAPIGitHubValidationService

ValidationResult 包含了四个验证结果:

enum ValidationResult {
    case ok(message: String)
    case empty
    case validating
    case failed(message: String)
}

分别是验证成功、验证为空、正在验证、验证失败。

在验证成功和验证失败两种情况中,会带上一个消息供展示。

SignupState 用于标记注册状态,表示是否已经注册,代码如下:

enum SignupState {
    case signedUp(signedUp: Bool)
}

协议 GitHubAPIGitHubValidationService 代码如下:

protocol GitHubAPI {
    func usernameAvailable(_ username: String) -> Observable<Bool>
    func signup(_ username: String, password: String) -> Observable<Bool>
}

protocol GitHubValidationService {
    func validateUsername(_ username: String) -> Observable<ValidationResult>
    func validatePassword(_ password: String) -> ValidationResult
    func validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult
}

在讨论这段代码的设计前,我们先思考一下哪些是异步场景:

  • 检查用户名是否可用
  • 注册

而验证密码和验证重复输入密码都可以同步地形式进行。

在设计到检查用户名注册时,应当返回一个 Observable 代替 callback ,而密码的验证只需要在一个方法中返回验证结果即可。

所以上述两个协议中 usernameAvailablesignupvalidateUsername 都是异步事件,都应当返回 Observable

DefaultImplementations.swift 文件给出了上述两个协议的实现,先来看 GitHubDefaultAPI

class GitHubDefaultAPI : GitHubAPI {
    let URLSession: Foundation.URLSession

    static let sharedAPI = GitHubDefaultAPI(
        URLSession: Foundation.URLSession.shared
    )

    init(URLSession: Foundation.URLSession) {
        self.URLSession = URLSession
    }

    func usernameAvailable(_ username: String) -> Observable<Bool> {
        // this is ofc just mock, but good enough

        let url = URL(string: "https://github.com/\(username.URLEscaped)")!
        let request = URLRequest(url: url)
        return self.URLSession.rx.response(request: request)
            .map { (response, _) in
                return response.statusCode == 404
            }
            .catchErrorJustReturn(false)
    }

    func signup(_ username: String, password: String) -> Observable<Bool> {
        // this is also just a mock
        let signupResult = arc4random() % 5 == 0 ? false : true
        return Observable.just(signupResult)
            .concat(Observable.never())
            .throttle(0.4, scheduler: MainScheduler.instance)
            .take(1)
    }
}

方法 usernameAvailable 验证了用户名是否可用,这里验证的方案是请求该用户名对应的主页,返回 404 说明没有该用户。

signup 是一个带延时的 mock 方法,对每一次的注册返回一个随机结果,并对该结果延迟 0.4s 。

你可能会问代码 .concat(Observable.never()) 存在的意义,回顾操作符 throttle ,当接到 completed 时,立即传递 completed。而 just 发射第一个值后立即发射 completed ,从而没有延时效果。当 concat 一个 never 时,Observable 永远不会发射 completed ,从而得到延时效果。

来看 GitHubDefaultValidationServiceGitHubDefaultValidationService 提供了用户名验证密码验证重复密码验证 三个功能。

我们只需关注方法 validateUsername

func validateUsername(_ username: String) -> Observable<ValidationResult> {
    if username.characters.count == 0 {
        return .just(.empty)
    }


    // this obviously won't be
    if username.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) != nil {
        return .just(.failed(message: "Username can only contain numbers or digits"))
    }

    let loadingValue = ValidationResult.validating

    return API
        .usernameAvailable(username)
        .map { available in
            if available {
                return .ok(message: "Username available")
            }
            else {
                return .failed(message: "Username already taken")
            }
        }
        .startWith(loadingValue)
}

首先验证输入的用户名是否为空,为空则直接返回 .just(.empty) ,再验证输入的用户名是否均为数字或父母,不是则直接返回 .just(.failed(message: "Username can only contain numbers or digits"))

当通过以上两种验证时,我们需要请求服务器验证用户名是否重复。.startWith(loadingValue) 为我们请求数据时添加了 loading 状态。

# UsingVanillaObservables > 1

本节示例在代码上使用 ObservableDriver 区别不大,以使用 Observable 代码为例。

GithubSignupViewModel1 是对应的ViewModel。

# ActivityIndicator

# Using 操作符

使用 using 操作符可以创建一个和 Observable 相同生命周期的实例对象·。

当 subscribe 时,创建该实例,当 dispose 时,调用该实例的dispose。

extension Observable where Element {
    public static func using<R: Disposable>(_ resourceFactory: @escaping () throws -> R, observableFactory: @escaping (R) throws -> Observable<E>) -> Observable<E>
}

resourceFactory 中传入一个工厂方法,返回一个可以 dispose 的实例。

observableFactory 中同样传入一个工厂方法,这里的 RresourceFactory 中返回的实例,返回一个 Observable ,这正是与 resource 对应生命周期的 Observable

Using

来看 ActivityIndicator 是如何使用 using 管理请求状态的。

extension ObservableConvertibleType {
    public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable<E> {
        return activityIndicator.trackActivityOfObservable(self)
    }
}

Observable 创建的扩展方法 trackActivity 中传入一个 ActivityIndicator 就可以跟踪加载状态了。

ActivityIndicator 服从协议 SharedSequenceConvertibleType ,直接调用 asObservable() 即可获取 loading 状态。

移除保证线程安全部分代码,ActivityIndicator代码如下:

public class ActivityIndicator : SharedSequenceConvertibleType {
    public typealias E = Bool
    public typealias SharingStrategy = DriverSharingStrategy

    private let _variable = Variable(0)
    private let _loading: SharedSequence<SharingStrategy, Bool>

    public init() {
        _loading = _variable.asDriver()
            .map { $0 > 0 }
            .distinctUntilChanged()
    }

    fileprivate func trackActivityOfObservable<O: ObservableConvertibleType>(_ source: O) -> Observable<O.E> {
        return Observable.using({ () -> ActivityToken<O.E> in
            self.increment()
            return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
        }) { t in
            return t.asObservable()
        }
    }

    private func increment() {
        _variable.value = _variable.value + 1
    }

    private func decrement() {
        _variable.value = _variable.value - 1
    }

    public func asSharedSequence() -> SharedSequence<SharingStrategy, E> {
        return _loading
    }
}

我们通过 _variable 表示正在执行的 Observable ,当 _variable 中的值为 0 时,_loading 发射一个 false ,表示加载结束,当 _variable 中的值大于 0 时,_loading 会发射 true

方法 incrementdecrement 处理的在执行的 Observable 的数量。

而在 trackActivityOfObservable 中使用了 usingincrementdecrementObservable 的生命周期绑定起来。

调用 usingresourceFactory 时,调用 increment 将资源数加1。 当 dispose 时,调用 ActivityTokendispose 方法。

ActivityToken 代码如下:

private struct ActivityToken<E> : ObservableConvertibleType, Disposable {
    private let _source: Observable<E>
    private let _dispose: Cancelable

    init(source: Observable<E>, disposeAction: @escaping () -> ()) {
        _source = source
        _dispose = Disposables.create(with: disposeAction)
    }

    func dispose() {
        _dispose.dispose()
    }

    func asObservable() -> Observable<E> {
        return _source
    }
}

这就完成了对 Observable 的监听,使用 trackActivity 可以监听任何一个 Observable 的生命周期,当我们需要依次发两个请求时,可以写类似如下代码:

let activity = ActivityIndicator()
someRequest()
    .flatMap(secondeRequest)
    .trackActivity(activity)

此时 activity 监听的是 someRequest().flatMap(secondeRequest) Observable ,当该 Observable 结束时,我们才会从 activity 中获取到请求结束状态。你可以很容易地在你的代码中插入各种状态监听,这不会对你的逻辑代码有任何影响。

# 参考阅读