RxExample GitHubSignup 部分代碼解讀
GitHubSignup 是一個注冊例子的 Demo ,同時也是一個 MVVM 的 Demo 。但本節將重點介紹代碼上 為什么這樣寫 ,你可以從中了解到何時在代碼中用 Rx 處理異步,如何合理的書寫代碼,以及如何優雅地處理網絡請求狀態。
事實上這個例子處理網絡請求的方式是使用 using 操作符 hook 網絡請求 Observable 的生命周期。
代碼均在 RxExample 項目中,相關涉及文件如下:
- GitHubSignup 文件夾所有內容
- ActivityIndicator.swift
我們先來簡單思考一下注冊需要注意哪幾個點,這里主要是表單驗證問題:
- 用戶名不能重復,需要提交用戶名到服務器驗證
- 注冊密碼有等級限制,比如長度、帶大小寫字母
- 兩次輸入的密碼相同
從 Protocols.swift 文件入手,這個文件有兩個枚舉 ValidationResult 和 SignupState ,兩個協議 GitHubAPI 和 GitHubValidationService 。
ValidationResult 包含了四個驗證結果:
enumValidationResult{ case ok(message: String) case empty case validating case failed(message: String) }
分別是驗證成功、驗證為空、正在驗證、驗證失敗。
在驗證成功和驗證失敗兩種情況中,會帶上一個消息供展示。
SignupState 用于標記注冊狀態,表示是否已經注冊,代碼如下:
enumSignupState{ case signedUp(signedUp: Bool) }
協議 GitHubAPI 和 GitHubValidationService 代碼如下:
protocolGitHubAPI{ funcusernameAvailable(_username: String) -> Observable<Bool> funcsignup(_username: String, password: String) -> Observable<Bool> } protocolGitHubValidationService{ funcvalidateUsername(_username: String) -> Observable<ValidationResult> funcvalidatePassword(_password: String) -> ValidationResult funcvalidateRepeatedPassword(_password: String, repeatedPassword: String) -> ValidationResult }
在討論這段代碼的設計前,我們先思考一下哪些是異步場景:
- 檢查用戶名是否可用
- 注冊
而驗證密碼和驗證重復輸入密碼都可以同步地形式進行。
在設計到 檢查用戶名 和 注冊 時,應當返回一個 Observable 代替 callback ,而密碼的驗證只需要在一個方法中返回驗證結果即可。
所以上述兩個協議中 usernameAvailable 、 signup 和 validateUsername 都是異步事件,都應當返回 Observable 。
DefaultImplementations.swift 文件給出了上述兩個協議的實現,先來看 GitHubDefaultAPI :
classGitHubDefaultAPI:GitHubAPI{ let URLSession: Foundation.URLSession static let sharedAPI = GitHubDefaultAPI( URLSession: Foundation.URLSession.shared ) init(URLSession: Foundation.URLSession) { self.URLSession = URLSession } funcusernameAvailable(_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) } funcsignup(_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 ,從而得到延時效果。
來看 GitHubDefaultValidationService , GitHubDefaultValidationService 提供了 用戶名驗證 、 密碼驗證 、 重復密碼驗證 三個功能。
我們只需關注方法 validateUsername :
funcvalidateUsername(_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
本節示例在代碼上使用 Observable 和 Driver 區別不大,以使用 Observable 代碼為例。
GithubSignupViewModel1 是對應的ViewModel。
ActivityIndicator
Using 操作符
使用 using 操作符可以創建一個和 Observable 相同生命周期的實例對象·。
當 subscribe 時,創建該實例,當 dispose 時,調用該實例的dispose。
extensionObservablewhereElement{ public static funcusing<R: Disposable>(_resourceFactory: @escaping() throws -> R, observableFactory: @escaping (R) throws -> Observable<E>) -> Observable<E> }
在 resourceFactory 中傳入一個工廠方法,返回一個可以 dispose 的實例。
在 observableFactory 中同樣傳入一個工廠方法,這里的 R 是 resourceFactory 中返回的實例,返回一個 Observable ,這正是與 resource 對應生命周期的 Observable 。
來看 ActivityIndicator 是如何使用 using 管理請求狀態的。
extensionObservableConvertibleType{ public functrackActivity(_activityIndicator: ActivityIndicator) -> Observable<E> { return activityIndicator.trackActivityOfObservable(self) } }
為 Observable 創建的擴展方法 trackActivity 中傳入一個 ActivityIndicator 就可以跟蹤加載狀態了。
ActivityIndicator 服從協議 SharedSequenceConvertibleType ,直接調用 asObservable() 即可獲取 loading 狀態。
移除保證線程安全部分代碼, ActivityIndicator 代碼如下:
public classActivityIndicator: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 functrackActivityOfObservable<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 funcincrement() { _variable.value = _variable.value + 1 } private funcdecrement() { _variable.value = _variable.value - 1 } public funcasSharedSequence() -> SharedSequence<SharingStrategy, E> { return _loading } }
我們通過 _variable 表示正在執行的 Observable ,當 _variable 中的值為 0 時, _loading 發射一個 false ,表示加載結束,當 _variable 中的值大于 0 時, _loading 會發射 true 。
方法 increment 和 decrement 處理的在執行的 Observable 的數量。
而在 trackActivityOfObservable 中使用了 using 將 increment 和 decrement 與 Observable 的生命周期綁定起來。
調用 using 的 resourceFactory 時,調用 increment 將資源數加1。
當 dispose 時,調用 ActivityToken 的 dispose 方法。
ActivityToken 代碼如下:
private structActivityToken<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) } funcdispose() { _dispose.dispose() } funcasObservable() -> Observable<E> { return _source } }
這就完成了對 Observable 的監聽。
來自:http://blog.dianqk.org/2017/03/03/rxexample-githubsignup/