RxJava與CallbackHell
在正式鋪展開本文內容之前,我們先思考一個問題:
你認為 RxJava 真的好用嗎,它好用在哪?
CallbackHell,中文翻譯為 回撥地獄,在以往沒有依賴RxJava
+ Retrofit
進行網路請求的程式碼中,這種程式碼並不少見(比如AsyncTask
),我曾有幸見識並維護了各種3層4層AsyncTask
回撥巢狀的專案——後來我一直拒絕閱讀AsyncTask
的原始碼,我想這應該是一個很重要的原因。
很感謝 @prototypez 的 《RxJava 沉思錄》 系列的文章,我個人認為它是 目前國內關於RxJava講解最好的系列 ,作者列舉了國內大多數文章中,關於RxJava好處的最常見的一些呼聲:
- 用到了觀察者模式
- 鏈式程式設計(一行程式碼實現XXX)
- 清晰且簡潔的程式碼
- 避免了Callback Hell
不可否認,這些的確都是RxJava優秀的閃光點,但我認為這不是核心,正如 這篇文章 所說的,其更重要的意義在於:
RxJava 給我們的事件驅動型程式設計帶來了新的思路,
RxJava
的Observable
一下子把我們的維度擴充到了時間和空間兩個維度。
事件驅動型程式設計這個詞很準確,現在我重新組織我的語言,”不要打破鏈式呼叫!“,這句話更應該說,不要破壞RxJava事件驅動型的程式設計思想。
你到底想說什麼?
現在讓我們回到文章的標題上,Android開發中,網路請求的錯誤處理一直是一個無法迴避的需求,有了隨著RxJava
+ Retrofit
的普及,難免會遇到這個問題:
這是我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。
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 並不優雅。
現在,如果有一種優雅的解決方案,那麼這種方案最好有哪些優點?
如有可能,我希望它能做到的是:
1.輕量級
輕量級意味著 較低的依賴成本,如果一個工具庫,它又要依賴若干個三方庫,首先apk體積的急速膨脹就令人無法接受。
2.靈活
靈活意味著 更低的遷移成本,我不希望,新增 或者 移除 這個工具令我的整個專案發生巨大的改動,甚至是重構。
如有可能,不要在已有的業務邏輯程式碼上進行修改。
3.低學習成本
低的學習成本 可以讓開發者更快的上手這個工具。
4.可高度擴充套件
如有可能,請讓這個工具庫能夠為所欲為。
這樣看來,上文中通過繼承的方式對全域性error的處理方案,存在著一定的侷限性,拋開令人瞠目結舌的回撥地獄之外,不能用lambda表示式 就已經讓我難以忍受。
RxWeaver: 一個輕量且靈活的全域性Error處理中介軟體
我花了一些時間開源了這個工具:
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個不同梯度的需求看看這個工具的韌性:
1.當接受到某種Error時,Toast對應的資訊展示給使用者
這是最常見的一種需求,當出現某種特殊異常(本案例以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解析異常!” 。
這似乎是一個很簡單的需求,我們提升一點難度:
2.當接收到某種Error時,彈出Dialog
這次需求是:
若接收到一個
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 依然可以勝任。
最後一個案例,讓我們再來一個更復雜的。
3.當接收到Token失效的Error時,跳轉login介面。
詳細需求是:
當接收到Token失效的Error時,跳轉login介面,使用者重新登入成功後,返回初始介面,並重新請求API;如果使用者登入失敗或取消登入,彈出錯誤資訊。
顯然這個邏輯有點複雜了, 對於實現這個需求來講,似乎不太現實,這次是否會束手無策呢?
fun test3() {
Observable.error(TokenExpiredException())
.compose(RxUtils.handleGlobalError<UserInfo>(this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
subscribe {
// ...
}
}
複製程式碼
這次我們把異常換成了TokenExpiredException
(因為直接例項化一個HttpException
過於複雜,所以我們自定義一個異常模擬代替它),我們直接看結果:
當然,無論怎麼重試,資料來源始終只會發射TokenExpiredException
,但是我們成功實現了這個看似複雜的需求。
4. 我想說明什麼?
我認為RxWeaver達到了我心目中的設計要求:
- 輕量級
你不需要擔心 RxWeaver 的體積,它足夠的輕量,輕量到所有類加起來只有不到200行程式碼,同時,除了RxJava
和RxAndroid
,它 沒有任何其它的依賴 ,體積大小隻有3kb。
- 靈活
RxWeaver 的配置不需要 修改 或者 刪除 任意一行已經存在的業務程式碼——它是完全可插拔的。
- 低學習成本
它的原理也是非常 簡單 的,只要熟悉了onErrorResumeNext
、retryWhen
、doOnError
這幾個關鍵的操作符,你就可以馬上上手對應的配置。
- 高擴充套件性
可以通過介面實現任意複雜的需求實現。
原理
這似乎本末倒置了,對於一個工具來說,熟練使用API 往往比 閱讀原始碼並瞭解原理 優先順序更高一些。但是我的想法是,如果你先了解了原理,這個工具的使用你會更加得心應手。
RxWeaver的原理複雜嗎?
實際上,RxWeaver的原始碼非常簡單,簡單到元件內部 沒有任何Error處理邏輯,所有的邏輯都交給使用者進行配置,它只是一個 中介軟體。
它的原理也是非常 簡單 的,只要熟悉了onErrorResumeNext
、retryWhen
、doOnError
這幾個關鍵的操作符,你就可以馬上上手對應的配置。
1.compose操作符
對於全域性異常的處理,我只需要在既有程式碼的 鏈式呼叫 加上一行程式碼,配置一個 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
中?
2.簡單的全域性異常處理:doOnError操作符
這個操作符的作用實在非常明顯了,就是當我們接收到某個 Throwable 時,想要做的邏輯:
這實在很適合大部分簡單的錯誤處理需求,就像上文的需求1一樣,當我們接收到某種指定的異常,彈出對應的message提示使用者,邏輯程式碼如下:
when (error) {
is JSONException -> {
Toast.makeText(activity, "全域性異常捕獲-Json解析異常!", Toast.LENGTH_SHORT).show()
}
else -> {
}
}
複製程式碼
這種錯誤的處理方式, 不會對既有的Observable進行變換 ,也就是說,JSONException
依然會最終傳遞到subscribe的 onError()
的回撥中——你依然需要實現 onError()
的回撥,哪怕什麼都不做,如有必要,再進行特殊的處理,否則會發生崩潰。
這種方式很簡單,但是涉及複雜的需求就無能為力了,這時候我們就需要藉助onErrorResumeNext
操作符了。
3.複雜的非同步Error處理: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>
中事件的值來判斷 是否重新請求網路資料 了。
4.重試的處理:retryWhen操作符
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)
,意味著不會重試。
5.似乎...完成了?
看來,僅僅需要這幾個操作符,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
也並沒有更復雜的邏輯,僅僅是對幾個操作符的組合使用而已。
此外的幾個類,也無非是對重試邏輯介面的封裝罷了。
6.如何實現介面的跳轉?
看到這裡,有的小夥伴可能已經有這樣一個疑問了:
需求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
對資料進行傳遞。
小結:RxJava,複雜還是簡單
在本文的開始,我簡單介紹了 RxWeaver 的幾個優點,其中一個是 極低的學習成本。
本文釋出之前,我把我的工具介紹給了一些剛接觸 RxJava 的開發者,他們接觸之後,反饋竟然出奇的統一:
你這個東西太難了!
對於這個結果,我很詫異,因為這畢竟只是一個加起來還不到200行的工具庫,後來我仔細的思考,我終於得出了一個結論,那就是:
本文的內容理解起來很 簡單 ,但首先需要你對RxJava有一定的理解,這比較 困難。
RxJava的學習曲線非常陡峭!正如 @prototypez 在他的 這篇文章 中所說的一樣:
RxJava 是一個 “夾帶了私貨” 的框架,它本身最重要的貢獻是提升了我們思考事件驅動型程式設計的維度,但是它與此同時又逼迫我們去接受了函數語言程式設計。
正如本文一開始所說的,我們已經習慣了 程式式程式設計 的思維,因此文中的一些 抽象的操作符 會讓我們陷入一定的迷茫,但是這也正是 RxJava 的魔力所在——它讓我不斷想要將新的需求 從更高層級進行抽象,嘗試寫出更簡潔的程式碼(至少在我看來)。
我非常喜歡 RxWeaver , 有朋友說說它程式碼有點少,但我卻認為 輕量 是它最大的優點,它的本質目的也正是幫助開發者 對業務邏輯進行組織,使其能夠寫出更 Reactive 和 Functional 的程式碼 。
--------------------------廣告分割線------------------------------
關於我
Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的部落格或者Github。
如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?