RxSwift使用过程中,不可避免的一个问题就是Error的处理,尤其是这种场景下:
button.rx.tap.flatMap {
requestNetwork()
}
.subscribe(onNext: {
xxx
}, onError: {
xxx
})
.disposed(by: disposeBag)
相信很多人初学RxSwift时都被这个问题困扰过:当网络请求成功时一切正常,但是一旦网络请求失败,问题就出现了:button点击没反应了。这是因为一旦网络请求失败,requestNetwork()便会收到一个error事件,从而打断订阅链。这个问题网上有不少解决方案,大体分为两种:
1.将序列再包装一层
比如RxSwift中文文档-Error Handling - 错误处理中提到的使用Result来处理,像这样:
button.rx.tap.flatMap { _ -> Observable<Result<XXX, Error>> in
requestNetwork()
.map{ Result.success($0) }
.catchError { error in Observable.just(Result.failure(error)) }
}
.subscribe(onNext: {
xxx
}, onError: {
xxx
})
.disposed(by: disposeBag)
或者这篇文章里建议的使用materialize来处理,大概就是:
let result = button.rx.tap.flatMap {
requestNetwork().materialize()
}
.shared()
result.elements().subscribe(onNext: {
xxx
})
.disposed(by: disposeBag)
result.errors().subscribe(onNext: {
xxx
})
.disposed(by: disposeBag)
思路差不多,总之就是将序列再包装一层,然后分别处理,注意其中.shared()的使用,因为这里会有两次订阅,如果不加.shared(),点击按钮后会发送两次请求。个人觉得这不是特别普适的一个处理方案,比如如果button点击之后需要依次调用多个接口请求:
button.rx.tap.flatMap {
requestNetwork()
}
.flatMap {
requestNetwork2($0)
}
.subscribe(onNext: {
xxx
}, onError: {
xxx
})
.disposed(by: disposeBag)
由于第二个请求依赖第一个的结果,而第一个请求现在返回的又是被包裹了一层的,这种场景下,无论是Result方案,还是materialize方案,处理起来都会显得有点繁琐了,因为首先要对第一个请求的事件进行解包。当然这还仅仅是两个请求,如果再多几个的话,整个处理起来就更麻烦了。
catchError/catchErrorJustReturn/asDriver/asSingal
这种方案就是将网络请求通过特定的操作符处理成不会失败的序列,如:
button.rx.tap.flatMap {
requestNetwork().catchErrorJustReturn(sampleResult)
}
.flatMap {
requestNetwork2($0).catchErrorJustReturn(sampleResult)
}
.subscribe(onNext: {
xxx
})
.disposed(by: disposeBag)
这也是我之前用过一段时间的处理方案,但是这个方案的最大问题在于:Error被丢失了。想要把这个error给捞出来,可以用do操作符来处理:
button.rx.tap.flatMap {
requestNetwork()
.do(onError: { xxx })
.catchErrorJustReturn(sampleResult)
}
.flatMap {
requestNetwork2($0)
.do(onError: { xxx })
.catchErrorJustReturn(sampleResult)
}
.subscribe(onNext: {
xxx
})
.disposed(by: disposeBag)
这么一看,代码又开始变得啰嗦了,每个flatMap中都要进行重复的处理,似乎还是不够优雅。
有没有更好的方案?
在考虑有没有更好的方案时,我们先思考一下:这个问题的根源是什么?
订阅是会断的,网络请求是会失败的
那我们先思考一下,这是否合理?非常合理,这就是真实的编程世界。而以上两个方案就是强行将会断的序列转化成不会断的序列,是否有些本末倒置?
那么解决订阅会断有没有更好的办法呢?私以为当然有,那就是:
重新订阅
怎么重新订阅呢?很简单,每次点击按钮都产生一次订阅(如果你有异议,很正常,先别急):
button.rx.tap.subscribe(onNext: {
doRequest()
})
.disposed(by: disposeBag)
func doRequest() {
requestNetwork().flatMap {
requestNetwork2($0)
}
.subscribe(onNext: {
xxx
}, onError: {
xxx
})
.disposed(by: disposeBag)
}
这样一来,每次点击按钮都会产生一个订阅,就不用担心请求失败造成订阅链断掉了。
那么这个方案有什么问题?
如果请求成功了怎么办?
成功了能有什么问题?当然有。
成功之后旧的订阅链不会断。是的,这就是这个方案目前来说最大的问题。不会断意味着资源不会释放,作为一个开发者,这个显然是不应该被接受的。
那咋办?
在请求成功的时候让它断掉不就完了?
很简单,在请求网络的方法中(这通常是咱们自己封装的统一方法,所以改动成本并不大),发送next之后再跟一个onCompleted()就好了。当然,有更好的方案: Single.简直就是为这种场景量身定做的特征序列。有兴趣的读者可以自行去查阅,和自己封装发送一个onCompleted()差不多。
总结下来就是:
每次点击按钮,产生一个订阅,在订阅产生next或error之后断掉链接,释放资源
这个方案可以让我们在写网络请求相关的API时不需要每次都啰嗦的去处理error,而可以在最终订阅时去统一处理(当然你想分开处理也是可以的)。
多说一点
那如果我们按钮点击之后有个确认弹框,或者有些个校验呢?像这样:
```swift
button.rx.tap.subscribe(onNext: {
doRequest()
})
.disposed(by: disposeBag)
func doRequest() {
observableAlert().flatMap
requestNetwork()
}
.flatMap {
requestNetwork2($0)
}
.subscribe(onNext: {
xxx
}, onError: {
xxx
})
.disposed(by: disposeBag)
}
func observableAlert() -> Observable<Void> {
Observable.create { observer in
presentAlert(onConfirmed: {
observer.onNext(())
}, onCanceled {
observer.onCompleted()
})
}
}
这里也容易出现一个问题,那就是弹框点击确定之后,订阅链断不了了。解决办法也很简单,和上面的网络请求一样,在onNext()之后加一个onCompleted()即可,或者使用Single.这里单独提出来是因为,比较容易忽略。
OK,以上就是笔者对于RxSwift Error处理的一些拙见,私以为相比以上两个方案来说,还是要简洁一些的。如果各位有更好的方案,或者对此方案有什么其他看法,欢迎批评指正🤗🤗