在正式鋪展開本文內容以前,咱們先思考一個問題:java
你認爲 RxJava 真的好用嗎,它好用在哪?android
CallbackHell,中文翻譯爲 回調地獄,在以往沒有依賴RxJava
+ Retrofit
進行網絡請求的代碼中,這種代碼並很多見(好比AsyncTask
),我曾有幸見識並維護了各類3層4層AsyncTask
回調嵌套的項目——後來我一直拒絕閱讀AsyncTask
的源碼,我想這應該是一個很重要的緣由。git
很感謝 @prototypez 的 《RxJava 沉思錄》 系列的文章,我我的認爲它是 目前國內關於RxJava講解最好的系列 ,做者列舉了國內大多數文章中,關於RxJava好處的最多見的一些呼聲:github
不能否認,這些的確都是RxJava優秀的閃光點,但我認爲這不是核心,正如 這篇文章 所說的,其更重要的意義在於:編程
RxJava 給咱們的事件驅動型編程帶來了新的思路,
RxJava
的Observable
一會兒把咱們的維度拓展到了時間和空間兩個維度。api
事件驅動型編程這個詞很準確,如今我從新組織個人語言,」不要打破鏈式調用!「,這句話更應該說,不要破壞RxJava事件驅動型的編程思想。網絡
如今讓咱們回到文章的標題上,Android開發中,網絡請求的錯誤處理一直是一個沒法迴避的需求,有了隨着RxJava
+ Retrofit
的普及,不免會遇到這個問題:app
這是我17年年初總結的一篇博客,那時我對於RxJava
的理解比較有限,我閱讀了網上不少前輩的博客,並總結了文中的這種方案,就是把全局的error處理放在onError()
中,並將Subscriber
包裝成MySubscriber
:異步
public abstract class MySubscriber<T> extends Subscriber<T> {
 // ...
@Override
public void onError(Throwable e) {
onError(ExceptionHandle.handleException(e)); // ExceptionHandle中就是全局處理的邏輯,詳情參考上方文章
}
public abstract void onError(ExceptionHandle.ResponeThrowable responeThrowable);
}
api.requestHttp() //網絡請求
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new MySubscriber<Model>(context) { // 包裝了全局error處理邏輯的MySubscriber
@Override
public void onNext(Model model) { // ... }
@Override
public void onError(ExceptionHandle.ResponeThrowable throwable) {
// .......
}
});
複製代碼
這種解決方案於我當時看來沒有問題,我認爲這應該就是 完美的解決方案 了吧。
很快我就意識到了另一個問題,就是這種方案成功地驅動我寫出了 RxJava版本的Callback Hell。
我不想大家笑話個人代碼,所以我決定先不把它們拋出來,來看一個常見的需求:
請求一個API,若是發生異常,彈出一個Dialog,詢問用戶是否重試,若是重試,從新請求這個API。
讓咱們看看可能不少開發者 第一直覺 會寫出的代碼(爲了保證代碼不那麼囉嗦,這裏我使用了Kotlin
):
api.requestHttp()
.subscribe(
onNext = {
// ...
},
onError = {
AlertDialog.Builder(context) // 彈出一個dialog,提示用戶是否重試
.xxxx
.setPositiveButton("重試") { _, _ -> // 點擊重試按鈕,從新請求
api.requestHttp()
.subscribe(
onNext = { ... },
onError = { ... }
)
}
.setNegativeButton("取消") { _, _ -> // 啥都不作 }
.show()
}
)
複製代碼
瞧!咱們寫出了什麼!
如今你也許明白了我當時的處境,onError()
和onComplete()
意味着此次訂閱事件的終止,若是全局的異常處理都放在onError()
中,接下來若是還有其餘的需求(好比網絡請求),就意味着你要在這個回調方法中再添加一層回調。
在一邊高呼RxJava
鏈式調用簡潔好用和 避免了CallbackHell 時,咱們將 響應式編程 扔到了一旁,而後繼續 按照平常的思惟 寫着 一模一樣的代碼。
若是你以爲這種操做徹底能夠接受,咱們能夠將需求升級一下:
若是發生異常,彈出dialog提示用戶重試,這種dialog最多可彈出3次。
好的,若是說,最多重試一次,讓代碼額外增長了1層回調的嵌套(其實是2層,Dialog的點擊事件自己也是一層回調),那麼最多重試3次,就是.....4層回調:
api.requestHttp()
.subscribe(
onNext = {
// ...
},
onError = {
api.requestHttp()
.subscribe(
onNext = {
// ...
},
onError = {
api.requestHttp()
.subscribe(
onNext = { ... },
onError = { ... } // 還有一層
)
}
)
}
)
複製代碼
你能夠說,我把這個請求封裝成一個函數,而後每次只調用函數就好了,話雖如此,你依然不可否認這種 CallbackHell 並不優雅。
若有可能,我但願它能作到的是:
輕量級意味着 較低的依賴成本,若是一個工具庫,它又要依賴若干個三方庫,首先apk體積的急速膨脹就使人沒法接受。
靈活意味着 更低的遷移成本,我不但願,添加 或者 移除 這個工具令個人整個項目發生巨大的改動,甚至是重構。
若有可能,不要在已有的業務邏輯代碼上進行修改。
低的學習成本 可讓開發者更快的上手這個工具。
若有可能,請讓這個工具庫可以隨心所欲。
這樣看來,上文中經過繼承的方式對全局error的處理方案,存在着必定的侷限性,拋開使人瞠目結舌的回調地獄以外,不能用lambda表達式 就已經讓我難以忍受。
我花了一些時間開源了這個工具:
RxWeaver: A lightweight and flexible error handler tools for RxJava2.
Weaver 翻譯過來叫作 織布鳥,我最初的目的也正是讓這個工具可以對邏輯代碼正確地組織,達到實現RxJava全局Error處理的需求。
爲了代碼的足夠簡潔,我選擇使用Kotlin做爲示範代碼,我保證你能夠看懂並理解它們——若是你的項目中適用的開發語言是
Java
,也請不用擔憂, RxWeaver 一樣提供了Java
版本的依賴和示例代碼,你能夠在這裏找到它。
RxWeaver的配置很是簡單,你只須要配置好對應的GlobalErrorTransformer
類,而後在須要處理error的網絡請求代碼中,經過compose()
操做符,將GlobalErrorTransformer
交給RxJava, 請注意,僅僅須要一行代碼:
private fun requestHttp() {
serviceManager.requestHttp() // 網絡請求
.compose(RxUtils.handleGlobalError<UserInfo>(this)) // 加上這行代碼
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe( // ....)
}
複製代碼
RxUtils.handleGlobalError<UserInfo>(this)
相似Java
中的靜態工具方法,它會返回一個對應GlobalErrorTransformer
的一個實例——裏面存儲的是對應的error處理邏輯,這個類並非 RxWeaver 的一部分,而是根據不一樣項目的不一樣業務,本身實現的一個類:
object RxUtils {
fun handleGlobalError(activity: FragmentActivity): GlobalErrorTransformer {
// ....
}
}
複製代碼
如今咱們須要知道的是,這樣一行代碼,能夠作到什麼樣的程度。
讓咱們從3個不一樣梯度的需求看看這個工具的韌性:
這是最多見的一種需求,當出現某種特殊異常(本案例以JSONException爲例),咱們會經過Toast提示這樣的消息給用戶:
全局異常捕獲-Json解析異常!
fun test() {
Observable.error(JSONException("JSONException"))
.compose(RxUtils.handleGlobalError<UserInfo>(this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
// ...
}
}
複製代碼
毫無疑問,當沒有加compose(RxUtils.handleGlobalError<UserInfo>(this))
這行代碼時,此次訂閱的結果必然是彈出一個 「onError:xxxx」的 toast。
如今咱們加上了compose的這行代碼,讓咱們拭目以待:
看起來成功了,即便咱們在onError()
裏面針對Exception
作出了單獨的處理,可是這個JSONException依然被全局捕獲了,並彈出了一個額外的toast :「全局異常捕獲-Json解析異常!」 。
這彷佛是一個很簡單的需求,咱們提高一點難度:
此次需求是:
若接收到一個
ConnectException
(鏈接異常),咱們讓彈出一個dialog,這個dialog只會彈一次,若用戶選擇重試,從新請求API
又回到了上文中這個可能會引起 Callback Hell 的需求,咱們疑問,如何保證 Dialog和重試邏輯正確執行的同時,不打破Observable流的連續性(鏈式調用)?
fun test2() {
Observable.error(ConnectException()) // 此次咱們把異常換成了`ConnectException`
.compose(RxUtils.handleGlobalError<UserInfo>(this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
// ...
}
}
複製代碼
依然是熟悉的代碼,此次咱們把異常換成了ConnectException
,咱們直接看結果:
由於咱們數據源是一個固定的ConnectException
,所以咱們不管怎麼重試,必然都只會接收到ConnectException
,這不重要,你發現沒有,即便是一個複雜的需求(彈出dialog,用戶選擇後,決定是否從新請求這個流),RxWeaver 依然能夠勝任。
最後一個案例,讓咱們再來一個更復雜的。
詳細需求是:
當接收到Token失效的Error時,跳轉login界面,用戶從新登陸成功後,返回初始界面,並從新請求API;若是用戶登陸失敗或取消登陸,彈出錯誤信息。
顯然這個邏輯有點複雜了, 對於實現這個需求來說,彷佛不太現實,此次是否會一籌莫展呢?
fun test3() {
Observable.error(TokenExpiredException())
.compose(RxUtils.handleGlobalError<UserInfo>(this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
subscribe {
// ...
}
}
複製代碼
此次咱們把異常換成了TokenExpiredException
(由於直接實例化一個HttpException
過於複雜,因此咱們自定義一個異常模擬代替它),咱們直接看結果:
固然,不管怎麼重試,數據源始終只會發射TokenExpiredException
,可是咱們成功實現了這個看似複雜的需求。
我認爲RxWeaver達到了我心目中的設計要求:
你不須要擔憂 RxWeaver 的體積,它足夠的輕量,輕量到全部類加起來只有不到200行代碼,同時,除了RxJava
和RxAndroid
,它 沒有任何其它的依賴 ,體積大小隻有3kb。
RxWeaver 的配置不須要 修改 或者 刪除 任意一行已經存在的業務代碼——它是徹底可插拔的。
它的原理也是很是 簡單 的,只要熟悉了onErrorResumeNext
、retryWhen
、doOnError
這幾個關鍵的操做符,你就能夠立刻上手對應的配置。
能夠經過接口實現任意複雜的需求實現。
這彷佛本末倒置了,對於一個工具來講,熟練使用API 每每比 閱讀源碼並瞭解原理 優先級更高一些。可是個人想法是,若是你先了解了原理,這個工具的使用你會更加駕輕就熟。
實際上,RxWeaver的源碼很是簡單,簡單到組件內部 沒有任何Error處理邏輯,全部的邏輯都交給用戶進行配置,它只是一個 中間件。
它的原理也是很是 簡單 的,只要熟悉了onErrorResumeNext
、retryWhen
、doOnError
這幾個關鍵的操做符,你就能夠立刻上手對應的配置。
對於全局異常的處理,我只須要在既有代碼的 鏈式調用 加上一行代碼,配置一個 GlobalErrorTransformer<T>
交給 compose()
操做符————這個操做符是 RxJava
給咱們提供的能夠面向 響應式數據類型 (Observable/Flowable/Single等等)進行 AOP 的接口, 能夠對響應式數據類型 加工 、修飾 ,甚至 替換。
這意味着,在既有的代碼上,使用compose()
操做符,我能夠將一段特殊處理的邏輯代碼插入到這個Observable
中,這實在太方便了。
對compose操做符不瞭解的同窗,請參考 【譯】避免打斷鏈式結構:使用.compose()操做符 @by小鄧子
compose()
操做符須要我傳入一個對應 響應式類型 (Observable/Flowable/Single等等)的Transformer
接口,可是問題是不一樣的 響應式類型 對應不一樣的 Transformer
接口,不一樣的因而咱們實現了一個通用的 GlobalErrorTransformer<T>
接口以 兼容不一樣響應式類型的事件流 :
class GlobalErrorTransformer<T> constructor(
private val globalOnNextRetryInterceptor: (T) -> Observable<T> = { Observable.just(it) },
private val globalOnErrorResume: (Throwable) -> Observable<T> = { Observable.error(it) },
private val retryConfigProvider: (Throwable) -> RetryConfig = { RetryConfig() },
private val globalDoOnErrorConsumer: (Throwable) -> Unit = { },
private val upStreamSchedulerProvider: () -> Scheduler = { AndroidSchedulers.mainThread() },
private val downStreamSchedulerProvider: () -> Scheduler = { AndroidSchedulers.mainThread() }
) : ObservableTransformer<T, T>, FlowableTransformer<T, T>, SingleTransformer<T, T>, MaybeTransformer<T, T>, CompletableTransformer {
// ...
}
複製代碼
如今咱們思考一下,若是咱們想把error處理的邏輯放在GlobalErrorTransformer
裏面,把這個GlobalErrorTransformer
交給compose()
操做符,就等於把error處理的邏輯所有 插入 到既有的Observable
事件流中了:
fun test() {
observable
.compose(RxUtils.handleGlobalError<UserInfo>(this)) // 插入異常處理邏輯
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
subscribe {
// ...
}
}
複製代碼
同理,若是某個API不須要追加全局異常處理的邏輯,只須要把這行代碼刪掉便可,不會影響其餘的業務代碼。
這是一個不錯的思路,接下來,咱們須要思考的是,如何將不一樣的異常處理邏輯加進GlobalErrorTransformer
中?
這個操做符的做用實在很是明顯了,就是當咱們接收到某個 Throwable 時,想要作的邏輯:
這實在很適合大部分簡單的錯誤處理需求,就像上文的需求1同樣,當咱們接收到某種指定的異常,彈出對應的message提示用戶,邏輯代碼以下:
when (error) {
is JSONException -> {
Toast.makeText(activity, "全局異常捕獲-Json解析異常!", Toast.LENGTH_SHORT).show()
}
else -> {
}
}
複製代碼
這種錯誤的處理方式, 不會對既有的Observable進行變換 ,也就是說,JSONException
依然會最終傳遞到subscribe的 onError()
的回調中——你依然須要實現 onError()
的回調,哪怕什麼都不作,若有必要,再進行特殊的處理,不然會發生崩潰。
這種方式很簡單,可是涉及複雜的需求就無能爲力了,這時候咱們就須要藉助onErrorResumeNext
操做符了。
以上文的需求2爲例,若接收到一個指定的異常,咱們需展現一個Dialog,提示用戶是否重試—— 這種狀況下,doOnError
操做符明顯無能爲力,由於它不具備 對Observable進行變換的能力。
這時就須要 onErrorResumeNext
操做符上場了,它的做用是:當流的事件傳遞過程當中發生了錯誤,咱們能夠將一個新的流交個 onErrorResumeNext
操做符,以保證事件流的繼續傳遞。
這是一個被嚴重低估的操做符,這個操做符意味着,只要你給一個Observable<T>
的,就能繼續往下傳遞事件,那麼,這和需求中的 展現一個Dialog供用戶選擇 有關係嗎?
固然有關係,咱們只須要把Dialog的事件轉換成對應的Observable
便可:
object RxDialog {
/** * 簡單的示例,彈出一個dialog提示用戶,將用戶的操做轉換爲一個流並返回 */
fun showErrorDialog(context: Context, message: String): Single<Boolean> {
return Single.create<Boolean> { emitter ->
AlertDialog.Builder(context)
.setTitle("錯誤")
.setMessage("您收到了一個異常:$message,是否重試本次請求?")
.setCancelable(false)
.setPositiveButton("重試") { _, _ -> emitter.onSuccess(true) }
.setNegativeButton("取消") { _, _ -> emitter.onSuccess(false) }
.show()
}
}
}
複製代碼
RxDialog的 showErrorDialog()
函數將會展現一個Dialog,返回值爲一個 Single<Boolean>
的流,當用戶點擊 肯定 按鈕,訂閱者會接收到一個 true
事件,反之,點擊 取消 按鈕,則會收到一個 false
事件。
RxJava還能這麼用?
固然,RxJava
所表明的是一種響應式的編程範式,在剛接觸RxJava的時候,咱們都見過這樣一種說法:RxJava 很是強大的一點即是 異步。
如今咱們回過頭來,網絡請求的數據流 表明的是一種異步,難道 彈出一個dialog,等待的用戶選擇結果 難道不也是一種異步嗎?
換句話說,網絡請求 的流中事件意味着 網絡請求的結果,那麼上文中的 Single<Boolean>
表明着流中的事件是 ** Dialog的點擊事件**。
其實RxJava發展的這些年來,Github上的RxJava擴展庫層出不窮,好比RxPermission
,RxBinding
等等等等,前者是將 權限請求 的結果做爲事件,交給了Observable
進行傳遞;後者則是將 **View對應的事件 ** (好比點擊事件,長按事件等等)交給了Observable
。
回過頭來,咱們如今經過RxDialog
建立了一個 響應式的Dialog,並獲取到了用戶的選擇結果Single<Boolean>
,接下來咱們須要作的就只是根據Single<Boolean>
中事件的值來判斷 是否從新請求網絡數據 了。
RxJava提供了 retryWhen()
操做符,交給咱們去處理是否從新執行流的訂閱(本文中就是指從新進行網絡請求):
篇幅所限,我不會針對這個操做符進行太多的講解,關於 retryWhen()
操做符,請參考:
【譯】對RxJava中.repeatWhen()和.retryWhen()操做符的思考 by 小鄧子
繼續上文的思路,咱們到了Dialog對應的Single<Boolean>
流,當用戶選擇後,實例化一個RetryConfig 對象,並把選擇的結果Single<Boolean>
交給了 condition
屬性:
RetryConfig(condition = RxDialog.showErrorDialog(params))
data class RetryConfig(
val maxRetries: Int = DEFAULT_RETRY_TIMES, // 最大重試次數,默認1
val delay: Int = DEFAULT_DELAY_DURATION, // 重試延遲,默認1000ms
val condition: () -> Single<Boolean> = { Single.just(false) } // 是否重試
)
複製代碼
如今讓咱們來從新整理一下思路:
1.當用戶接收到一個指定的異常時,彈出一個Dialog,其選擇結果爲Single<Boolean>
;
2.RetryConfig
內部存儲了一個Single<Boolean>
的屬性,這是一個決定了是否重試的函數;
3.當用戶選擇了確認按鈕,將Single(true)
交給並實例化一個RetryConfig
,這意味着會重試,若是選擇了取消,則爲Single(false)
,意味着不會重試。
看來,僅僅須要這幾個操做符,Error處理複雜的需求咱們已經可以實現了?
的確如此,實際上,GlobalErrorTransformer
內部的處理,也正是調用這幾個操做符:
class GlobalErrorTransformer<T> constructor(
private val globalOnNextRetryInterceptor: (T) -> Observable<T> = { Observable.just(it) },
private val globalOnErrorResume: (Throwable) -> Observable<T> = { Observable.error(it) },
private val retryConfigProvider: (Throwable) -> RetryConfig = { RetryConfig() },
private val globalDoOnErrorConsumer: (Throwable) -> Unit = { },
private val upStreamSchedulerProvider: () -> Scheduler = { AndroidSchedulers.mainThread() },
private val downStreamSchedulerProvider: () -> Scheduler = { AndroidSchedulers.mainThread() }
) : ObservableTransformer<T, T>,
FlowableTransformer<T, T>,
SingleTransformer<T, T>,
MaybeTransformer<T, T>,
CompletableTransformer {
override fun apply(upstream: Observable<T>): Observable<T> =
upstream
.flatMap {
globalOnNextRetryInterceptor(it)
}
.onErrorResumeNext { throwable: Throwable ->
globalOnErrorResume(throwable)
}
.observeOn(upStreamSchedulerProvider())
.retryWhen(ObservableRetryDelay(retryConfigProvider))
.doOnError(globalDoOnErrorConsumer)
.observeOn(downStreamSchedulerProvider())
// 其餘響應式類型同理...
}
複製代碼
這也正是 RxWeaver 這個工具爲何如此 輕量 的緣由,即便是 核心類 GlobalErrorTransformer
也並無更復雜的邏輯,僅僅是對幾個操做符的組合使用而已。
此外的幾個類,也無非是對重試邏輯接口的封裝罷了。
看到這裏,有的小夥伴可能已經有這樣一個疑問了:
需求2中的,Dialog的邏輯我可以理解,那麼,需求3中,Token失效,跳轉login並返回重試是如何實現的?
實際上,不管是 網絡請求 , 仍是 彈出Dialog , 亦或者 跳轉Login,其終究只是一個 事件的流 而已,前者能經過接口返回一個 Observble<T>
或者 Single<T>
, 跳轉Login 固然也能夠:
class NavigatorFragment : Fragment() {
fun startLoginForResult(activity: FragmentActivity): Single<Boolean> {
// ....
}
}
複製代碼
篇幅所限,本文不進行實現代碼的展現,源碼請參考這裏。
其原理和 RxPermissions 、RxLifecycle 還有筆者的 RxImagePicker 徹底同樣,依靠一個不可見的Fragment
對數據進行傳遞。
在本文的開始,我簡單介紹了 RxWeaver 的幾個優勢,其中一個是 極低的學習成本。
本文發佈以前,我把個人工具介紹給了一些剛接觸 RxJava 的開發者,他們接觸以後,反饋居然出奇的統一:
你這個東西太難了!
對於這個結果,我很詫異,由於這畢竟只是一個加起來還不到200行的工具庫,後來我仔細的思考,我終於得出了一個結論,那就是:
本文的內容理解起來很 簡單 ,但首先須要你對RxJava有必定的理解,這比較 困難。
RxJava的學習曲線很是陡峭!正如 @prototypez 在他的 這篇文章 中所說的同樣:
RxJava 是一個 「夾帶了私貨」 的框架,它自己最重要的貢獻是提高了咱們思考事件驅動型編程的維度,可是它與此同時又逼迫咱們去接受了函數式編程。
正如本文一開始所說的,咱們已經習慣了 過程式編程 的思惟,所以文中的一些 抽象的操做符 會讓咱們陷入必定的迷茫,可是這也正是 RxJava 的魔力所在——它讓我不斷想要將新的需求 從更高層級進行抽象,嘗試寫出更簡潔的代碼(至少在我看來)。
我很是喜歡 RxWeaver , 有朋友說說它代碼有點少,但我卻認爲 輕量 是它最大的優勢,它的本質目的也正是幫助開發者 對業務邏輯進行組織,使其可以寫出更 Reactive 和 Functional 的代碼 。
--------------------------廣告分割線------------------------------
Hello,我是卻把清梅嗅,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人博客或者Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?