如何处理 RxSwift 中的错误 20170321@Adam Borek

90 阅读4分钟

原文:How to handle errors in RxSwift

最近,MVVM 成为 iOS 应用程序非常流行的架构设计。尤其是当 RxSwift 开始越来越受欢迎的时候。在 RxMVVM 中,大部分属性都是通过 Observables 表达的。

不过,每当观察对象收到 errorcompleted 事件时,它们就会终止。终止意味着 Observable 的订阅者不会收到任何新消息。当我开始学习 Rx 时,我并没有意识到这一规则的后果。

你在错误处理方面遇见过问题吗?您的 Observable 是否意外终止,导致按钮不再发送点击事件?这就是本文的主题。祝您阅读愉快 📚💪

Errors - 示例

当用户点击按钮时,移动应用程序通常会执行一些 API 请求。我们的示例将涵盖这种情况:

点击 success 时会调用假 API 请求,并得到成功的答复。同样,点击 failure 按钮也会伪造错误。点击按钮应增加计数。

这就是我想通过此图实现的目标:

successTap        -s-----s--s-----s---------->  
failureTap        ----f---f-----f----f------->  
buttonTaps<Bool>  -T--F--TF-F---F-T--F------->  
response          --V--E--VE-V---E-V--E------>  
(using flatMap)  

where:  
's' and 'f' - success or failure button tap  
'T' and 'F' - true or false  
'V' - success response  
'E' - failure response (error)

编码时间 - 第 1 次尝试

让我们编写一些代码。您需要将点击成功按钮映射为 true 事件,将点击失败按钮映射为 false 事件。然后,将它们 merge() 为一个 Observable

如果这是你第一次使用 Rx 和 merge()map()flatMap(),而且感觉很陌生,请先阅读RxSwift 编程思想。我将在那里介绍如何以反应式方式思考,以及如何使用。

事实上,点击 success 按钮确实会增加成功次数。但是,只要点击 failure 按钮,整个可观察对象链就会自行 dispose。接下来,点击 success 按钮不再会增加成功次数🙀。

你会问为什么?

performAPICall 失败时,它会返回一个错误事件(与真正的 API 调用相同)。由于我们使用的是 flatMap,因此内部 Observable 中的所有 nexterror 都会传递到主序列中。

因此,主 Observable 序列会收到一个错误事件,同时也会终止💀⚰。

RxSwift 与错误 -- 如何处理?

有时,错误正是你所期望发生的。以登录表单为例。如果密码与给定的电子邮件不匹配,服务器就会返回错误。这是一个预料之中的错误,天哪,这下可好,错误来了!👌

如果错误不是异常,就不应该结束 Observable 序列。要做到这一点,你的 API 调用应返回 Observable<Result<T>>。将 Result<T> 作为 next 事件不会终止主 Observable 序列。

不过,还有一种更简单的方法。RxSwiftExt 提供了 materialize 操作符。它将 Observable<T> 转换为 Observable<Event<T>>,后者有两个附加操作符:

  • elements() 返回 Observable<T>
  • errors() 返回 Observable<Error>

有了这两个观察项,您就可以按照自己的意愿处理 API 错误了:

😱 - performAPICall() 被调用两次

上述解决方案正如我们所期望的那样有效,但其中存在一个错误。每当你按下任何按钮时,都会调用两次 performAPICall()。要查看这一点,必须在 performAPICall() 中设置一个断点。

你可能会说这在我们的示例中没什么大不了的,但在现实生活中,调用一个方法 2 次会向服务器发送 2 个请求,这就不好了。要解决这个问题,需要在 result Observable 中使用 share() 操作符。其余部分不变:

何去何从?

使用 RxSwift 扩展为用户界面提供信息时,处理错误并不像您最初想象的那样简单。即使错误来自内部的 flatMap,错误事件也会破坏 Observable

通常,您希望将错误通知给用户。要做到这一点,必须将错误视为预期会发生的事情,而不是异常。我建议使用 RxSwiftExt 中的 materialize(),但不要忘记使用 share()🙂。您不会希望向 API 发送 2 个请求😉。

您可以在此处找到项目示例。

如果你想了解有关 share() 操作符的更多信息,有一篇很不错的文章。

GitHub 上的一个问题提到了更多关于错误和不存在普遍错误的观点。该演讲令人大开眼界。我认为值得一读。

您喜欢这篇文章吗?请点击下面的按钮进行分享。🍺

References

标题图片 - dribbble.com - Artur Martynowski @ All in Mobile

附:本文源码

//
//  ViewController.swift
//  RxErrorHandling
//
//  Created by Adam Borek on 01.03.2017.
//  Copyright © 2017 Adam Borek. All rights reserved.
//

import UIKit
import RxSwift
import RxSwiftExt
import RxCocoa

struct SampleError: Swift.Error {}

class ViewController: UIViewController {
    let disposeBag = DisposeBag()
    @IBOutlet weak var successessCountLabel: UILabel!
    @IBOutlet weak var failuresCountLabel: UILabel!
    @IBOutlet weak var successButton: UIButton!
    @IBOutlet weak var failureButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        //failWillCloseTheStream()
        usingMaterialize()
    }

    private func failWillCloseTheStream() {
        let successesCount = Observable
            .of(successButton.rx.tap.map { true }, failureButton.rx.tap.map { false })
            .merge()
            .flatMap { [unowned self] performWithSuccess in
                return self.performAPICall(shouldEndWithSuccess: performWithSuccess)
            }.scan(0) { accumulator, _ in
                return accumulator + 1
            }.map { "\($0)" }

        successesCount.bindTo(successessCountLabel.rx.text)
            .disposed(by: disposeBag)
    }

    private func usingMaterialize() {
        let result = Observable
            .of(successButton.rx.tap.map { true }, failureButton.rx.tap.map { false })
            .merge()
            .flatMap { [unowned self] performWithSuccess in
                return self.performAPICall(shouldEndWithSuccess: performWithSuccess)
                    .materialize()
            }.share()

        result.elements()
            .scan(0) { accumulator, _ in
                return accumulator + 1
            }.map { "\($0)" }
            .bindTo(successessCountLabel.rx.text)
            .disposed(by: disposeBag)

        result.errors()
            .scan(0) { accumulator, _ in
                return accumulator + 1
            }.map { "\($0)" }
            .bindTo(failuresCountLabel.rx.text)
            .disposed(by: disposeBag)
    }

    private func performAPICall(shouldEndWithSuccess: Bool) -> Observable<Void> {
        if shouldEndWithSuccess {
            return .just(())
        } else {
            return .error(SampleError())
        }
    }
}