本文是 "RxJava 沉思錄" 系列的第三篇分享育苟。本系列所有分享:
在上一篇分享中晋渺,我們應該已經(jīng)對 Observable 在空間維度上重新組織事件的能力 印象深刻了,那么自然而然的脓斩,我們?nèi)菀茁?lián)想到時間維度木西,事實上就我個人而言,我認為 Observable 在時間維度上的重新組織事件的能力 相比較其空間維度的能力更為突出随静。與上一篇類似八千,本文接下來將通過列舉真實的例子來闡述這一論點。
點擊事件防抖動
這是一個比較常見的情景燎猛,用戶在手機比較卡頓的時候恋捆,點擊某個按鈕,正常應該啟動一個頁面扛门,但是手機比較卡鸠信,沒有立即啟動,用戶就點了好幾下论寨,結(jié)果等手機回過神來的時候星立,就會啟動好幾個一樣的頁面。
這個需求用 Callback 的方式比較難處理葬凳,但是相信用過 RxJava 的開發(fā)者都知道怎么處理:
RxView.clicks(btn)
.debounce(500, TimeUnit.MILLISECONDS)
.observerOn(AndroidSchedulers.mainThread())
.subscribe(o -> {
// handle clicks
})
debounce
操作符產(chǎn)生一個新的Observable
, 這個Observable
只發(fā)射原Observable
中時間間隔小于指定閾值的最大子序列的最后一個元素绰垂。 參考資料:Debounce
雖然這個例子比較簡單,但是它很好的表達了 Observable 可以在時間維度上對其發(fā)射的事件進行重新組織 , 從而做到之前 Callback 形式不容易做到的事情火焰。
社交軟件上消息的點贊與取消點贊
點贊與取消點贊是社交軟件上經(jīng)常出現(xiàn)的需求劲装,假設我們目前有下面這樣的點贊與取消點贊的代碼:
boolean like;
likeBtn.setOnClickListener(v -> {
if (like) {
// 取消點贊
sendCancelLikeRequest(postId);
} else {
// 點贊
sendLikeRequest(postId);
}
like = !like;
});
以下圖片素材資源來自 Dribbble
如果你碰巧實現(xiàn)了一個非常酷炫的點贊動畫昌简,用戶可能會玩得不亦樂乎占业,這個時候可能會對后端服務器造成一定的壓力,因為每次點贊與取消點贊都會發(fā)起網(wǎng)絡請求纯赎,假如很多用戶同時在玩這個點贊動畫谦疾,服務器可能會不堪重負。
和前一個例子的防抖動思路差不多犬金,我們首先想到需要防抖動:
boolean like;
PublishSubject<Boolean> likeAction = PublishSubject.create();
likeBtn.setOnClickListener(v -> {
likeAction.onNext(like);
like = !like;
});
likeAction.debounce(1000, TimeUnit.MILLISECONDS)
.observerOn(AndroidSchedulers.mainThread())
.subscribe(like -> {
if (like) {
sendCancelLikeRequest(postId);
} else {
sendLikeRequest(postId);
}
});
寫到這個份上念恍,其實已經(jīng)可以解決服務器壓力過大的問題了,但是還是有優(yōu)化空間晚顷,假設當前是已贊狀態(tài)峰伙,用戶快速點擊 2 下,按照上面的代碼该默,還是會發(fā)送一次點贊的請求瞳氓,由于當前是已贊狀態(tài),再發(fā)送一次點贊請求是沒有意義的栓袖,所以我們優(yōu)化的目標就是將這一類事件過濾掉:
Observable<Boolean> debounced = likeAction.debounce(1000, TimeUnit.MILLISECONDS);
debounced.zipWith(
debounced.startWith(like),
(last, current) -> last == current ? new Pair<>(false, false) : new Pair<>(true, current)
)
.flatMap(pair -> pair.first ? Observable.just(pair.second) : Observable.empty())
.subscribe(like -> {
if (like) {
sendCancelLikeRequest(postId);
} else {
sendLikeRequest(postId);
}
});
zipWith
操作符可以把兩個Observable
發(fā)射的相同序號(同為第 x 個)的元素匣摘,進行運算轉(zhuǎn)換锅锨,得到的新元素作為新的Observable
對應序號所發(fā)射的元素。參考資料:ZipWith
上面的代碼恋沃,我們可以看到必搞,首先我們對事件流做了一次 debounce
操作,得到 debounced
事件流囊咏,然后我們把 debounced
事件流和 debounced.startWith(like)
事件流做了一次 zipWith
操作恕洲。相當于新的這個 Observable
中發(fā)射的第 n 個元素(n >= 2)是由 debounced
事件流中的第 n 和 第 n-1 個元素運算得到的(新的這個 Observable
中發(fā)射的第 1 個元素是由 debounced
事件流中的第 1 個元素和原始點贊狀態(tài) like
運算而來)。
運算的結(jié)果是得到一個 Pair
對象梅割,它是一個雙布爾類型二元組霜第,二元組第一個元素為 true 代表這個事件不該被忽略,應該被觀察者觀察到户辞;若為 false 則應該被忽略泌类。二元組的第二個元素僅在第一個元素為 true 的情況下才有意義,true 表示應該發(fā)起一次點贊操作底燎,而 false 表示應該發(fā)起一次取消點贊操作刃榨。上面提到的“運算”具體運算的規(guī)則是,比較兩個元素双仍,若相等枢希,則把二元組的第一個元素置為 false,若不相等朱沃,則把二元組的第一個元素置為 true, 同時把二元組的第二個元素置為 debounced
事件流發(fā)射的那個元素苞轿。
隨后的 flatMap
操作符完成了兩個邏輯,一是過濾掉二元組第一個元素為 false 的二元組逗物,二是把二元組轉(zhuǎn)化回最初的 Boolean
事件流搬卒。其實這個邏輯也可由 filter
和 map
兩個操作符配合完成,這里為了簡單用了一個操作符翎卓。
雖然上面用了不少篇幅解釋了每個操作符的意義契邀,但其實核心思想是簡單的,就是在原先 debounce
操作符的基礎上莲祸,把得到的事件流里每個元素和它的上一個元素做比較蹂安,如果這個元素和上個元素相同(例如在已贊狀態(tài)下再次發(fā)起點贊操作), 就把這個元素過濾掉椭迎,這樣最終的觀察者里只會在在真正需要改變點贊狀態(tài)的時候才會發(fā)起網(wǎng)絡請求了锐帜。
我們考慮用 Callback 實現(xiàn)相同邏輯,雖然比較本次操作與上次操作這樣的邏輯通過 Callback 也可以做到畜号,但是 debounce
這個操作符完成的任務缴阎,如果要使用 Callback 來實現(xiàn)就非常復雜了,我們需要定義一個計時器简软,還要負責啟動與關(guān)閉這個計時器蛮拔,我們的 Callback 內(nèi)部會摻雜進很多和觀察者本身無關(guān)的邏輯述暂,相比 RxJava 版本的純粹相去甚遠。
檢測雙擊事件
首先建炫,我們需要定義雙擊事件畦韭,不妨先規(guī)定兩次點擊小于 500 毫秒則為一次雙擊事件。我們先使用 Callback 的方式實現(xiàn):
long lastClickTimeStamp;
btn.setOnClickListener(v -> {
if (System.currentTimeMillis() - lastClickTimeStamp < 500) {
// handle double click
}
});
上面的代碼很容易理解肛跌,我們引入一個中間變量 lastClickTimeStamp
, 通過比較點擊事件發(fā)生時和上一次點擊事件的時間差是否小于 500 毫秒艺配,來確認是否發(fā)生了一次雙擊事件。那么如何通過 RxJava 來實現(xiàn)呢衍慎?就和上一個例子一樣转唉,我們可以在時間維度對 Observable
發(fā)射的事件進行重新組織,只過濾出與上次點擊事件間隔小于 500 毫秒的點擊事件稳捆,代碼如下:
Observable<Long> clicks = RxView.clicks(btn)
.map(o -> System.currentTimeMillis())
.share();
clicks.zipWith(clicks.skip(1), (t1, t2) -> t2 - t1)
.filter(interval -> interval < 500)
.subscribe(o -> {
// handle double click
});
我們再一次用到了 zipWith
操作符來對事件流自身相鄰的兩個元素做比較赠法,另外這次代碼中使用了 share
操作符,用來保證點擊事件的 Observable
被轉(zhuǎn)為 Hot Observable乔夯。
在
RxJava
中砖织,Observable
可以被分為Hot Observable
與Cold Observable
,引用《Learning Reactive Programming with Java 8》中一個形象的比喻(翻譯后的意思):我們可以這樣認為,Cold Observable
在每次被訂閱的時候為每一個Subscriber
單獨發(fā)送可供使用的所有元素末荐,而Hot Observable
始終處于運行狀態(tài)當中镶苞,在它運行的過程中,向它的訂閱者發(fā)射元素(發(fā)送廣播鞠评、事件)茂蚓,我們可以把Hot Observable
比喻成一個電臺,聽眾從某個時刻收聽這個電臺開始就可以聽到此時播放的節(jié)目以及之后的節(jié)目剃幌,但是無法聽到電臺此前播放的節(jié)目聋涨,而Cold Observable
就像音樂 CD ,人們購買 CD 的時間可能前后有差距负乡,但是收聽 CD 時都是從第一個曲目開始播放的牍白。也就是說同一張 CD ,每個人收聽到的內(nèi)容都是一樣的抖棘, 無論收聽時間早或晚茂腥。
僅僅是上面這個雙擊檢測的例子,還不能體現(xiàn) RxJava 的優(yōu)越性切省,我們把需求改得更復雜一點:如果用戶在“短時間”內(nèi)連續(xù)多次點擊最岗,只能算一次雙擊操作。這個需求是合理的朝捆,因為如果按照上面 Callback 的寫法般渡,雖然可以檢測出雙擊操作,但是如果用戶快速點擊 n 次(間隔均小于 500 毫秒,n >= 2), 就會觸發(fā) n - 1 次雙擊事件驯用,假設雙擊處理函數(shù)里需要發(fā)起網(wǎng)絡請求脸秽,會對服務器造成壓力。要實現(xiàn)這個需求其實也簡單蝴乔,和上一個例子類似记餐,我們用到了 debounce
操作符:
Observable<Object> clicks = RxView.clicks(btn).share()
clicks.buffer(clicks.debounce(500, TimeUnit.MILLISECONDS))
.filter(events -> events.size >= 2)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> {
// handle double click
});
buffer
操作符接受一個Observable
為參數(shù),這個Observable
所發(fā)射的元素是什么不重要薇正,重要的是這些元素發(fā)射的時間點剥扣,這些時間點會在時間維度上把原來那個Observable
所發(fā)射的元素劃分為一系列元素的組,buffer
操作符返回的新的Observable
發(fā)射的元素即為那些“組”铝穷。
參考資料: Buffer
上面的代碼通過 buffer
和 debounce
兩個操作符很巧妙的把點擊事件流轉(zhuǎn)化為了我們關(guān)心的 “短時間內(nèi)點擊次數(shù)超過 2 次” 的事件流钠怯,而且新的事件流中任意兩個相鄰事件間隔必定大于 500 毫秒。
在這個例子中曙聂,如果我們想要使用 Callback 去實現(xiàn)相似邏輯晦炊,代碼量肯定是巨大的,而且魯棒性也無法保證宁脊。
搜索提示
我們平時使用的搜索框中断国,常常是當用戶輸入一部分內(nèi)容后,下方就會顯示對應的搜索提示榆苞,以支付寶為例稳衬,當在搜索框輸入“螞蟻”關(guān)鍵詞后,下方自動刷新和關(guān)鍵詞相關(guān)的結(jié)果:
為了簡化這個例子坐漏,我們不妨定義根據(jù)關(guān)鍵詞搜索的接口如下:
public interface Api {
@GET("path/to/api")
Observable<List<String>> queryKeyword(String keyword);
}
查詢接口現(xiàn)在已經(jīng)確定下來薄疚,我們考慮一下在實現(xiàn)這個需求的過程中需要考慮哪些因素:
- 防止用戶輸入過快,觸發(fā)過多網(wǎng)絡請求赊琳,需要對輸入事件做一下防抖動街夭。
- 用戶在輸入關(guān)鍵詞過程中可能觸發(fā)多次請求,那么躏筏,如果后一次請求的結(jié)果先返回板丽,前一次請求的結(jié)果后返回,這種情況應該保證界面展示的是后一次請求的結(jié)果趁尼。
- 用戶在輸入關(guān)鍵詞過程中可能觸發(fā)多次請求埃碱,那么,如果后一次請求的結(jié)果返回時酥泞,前一次請求的結(jié)果尚未返回的情況下砚殿,就應該取消前一次請求。
綜合考慮上面的因素以后婶博,我們使用 RxJava 實現(xiàn)的對應的代碼如下:
RxTextView.textChanges(input)
.debounce(300, TimeUnit.MILLISECONDS)
.switchMap(text -> api.queryKeyword(text.toString()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(results -> {
// handle results
});
switchMap
這個操作符與flatMap
操作符類似瓮具,但是區(qū)別是如果原Observable
中的兩個元素,通過switchMap
操作符都轉(zhuǎn)為Observable
之后凡人,如果后一個元素對應的Observable
發(fā)射元素時名党,前一個元素對應的Observable
尚未發(fā)射完所有元素,那么前一個元素對應的Observable
會被自動取消訂閱挠轴,尚未發(fā)射完的元素也不會體現(xiàn)在switchMap
操作符調(diào)用后產(chǎn)生的新的Observable
發(fā)射的元素中传睹。
參考資料:SwitchMap
我們分析上面的代碼,可以發(fā)現(xiàn): debounce
操作符解決了問題 1岸晦,switchMap
操作符解決了問題 2欧啤、3。這個例子可以很好的說明启上,RxJava 的 Observable
可以通過一系列操作符從時間的維度上重新組織事件邢隧,從而簡化觀察者的邏輯。這個例子如果使用 Callback 來實現(xiàn)冈在,肯定是十分復雜的倒慧,需要設置計時器以及一堆中間變量,觀察者中也會摻雜進很多額外的邏輯包券,用來保證事件與事件的依賴關(guān)系纫谅。
(未完待續(xù))
本文屬于 "RxJava 沉思錄" 系列,歡迎閱讀本系列的其他分享: