RxJava 沉思錄(三):時間維度

本文是 "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

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 事件流搬卒。其實這個邏輯也可由 filtermap 兩個操作符配合完成,這里為了簡單用了一個操作符翎卓。

雖然上面用了不少篇幅解釋了每個操作符的意義契邀,但其實核心思想是簡單的,就是在原先 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 ObservableCold 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

上面的代碼通過 bufferdebounce 兩個操作符很巧妙的把點擊事件流轉(zhuǎn)化為了我們關(guān)心的 “短時間內(nèi)點擊次數(shù)超過 2 次” 的事件流钠怯,而且新的事件流中任意兩個相鄰事件間隔必定大于 500 毫秒。

在這個例子中曙聂,如果我們想要使用 Callback 去實現(xiàn)相似邏輯晦炊,代碼量肯定是巨大的,而且魯棒性也無法保證宁脊。

搜索提示

我們平時使用的搜索框中断国,常常是當用戶輸入一部分內(nèi)容后,下方就會顯示對應的搜索提示榆苞,以支付寶為例稳衬,當在搜索框輸入“螞蟻”關(guān)鍵詞后,下方自動刷新和關(guān)鍵詞相關(guān)的結(jié)果:

image

為了簡化這個例子坐漏,我們不妨定義根據(jù)關(guān)鍵詞搜索的接口如下:

public interface Api {
    @GET("path/to/api")
    Observable<List<String>> queryKeyword(String keyword);
}

查詢接口現(xiàn)在已經(jīng)確定下來薄疚,我們考慮一下在實現(xiàn)這個需求的過程中需要考慮哪些因素:

  1. 防止用戶輸入過快,觸發(fā)過多網(wǎng)絡請求赊琳,需要對輸入事件做一下防抖動街夭。
  2. 用戶在輸入關(guān)鍵詞過程中可能觸發(fā)多次請求,那么躏筏,如果后一次請求的結(jié)果先返回板丽,前一次請求的結(jié)果后返回,這種情況應該保證界面展示的是后一次請求的結(jié)果趁尼。
  3. 用戶在輸入關(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 沉思錄" 系列,歡迎閱讀本系列的其他分享:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末付秕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子侍郭,更是在濱河造成了極大的恐慌询吴,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亮元,死亡現(xiàn)場離奇詭異汰寓,居然都是意外死亡,警方通過查閱死者的電腦和手機苹粟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門有滑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嵌削,你說我怎么就攤上這事毛好。” “怎么了苛秕?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵肌访,是天一觀的道長。 經(jīng)常有香客問我艇劫,道長吼驶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮蟹演,結(jié)果婚禮上风钻,老公的妹妹穿的比我還像新娘。我一直安慰自己酒请,他們只是感情好骡技,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著羞反,像睡著了一般布朦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昼窗,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天是趴,我揣著相機與錄音,去河邊找鬼澄惊。 笑死右遭,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的缤削。 我是一名探鬼主播窘哈,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼亭敢!你這毒婦竟也來了滚婉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤帅刀,失蹤者是張志新(化名)和其女友劉穎让腹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扣溺,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡骇窍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了锥余。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腹纳。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖驱犹,靈堂內(nèi)的尸體忽然破棺而出嘲恍,到底是詐尸還是另有隱情,我是刑警寧澤雄驹,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布佃牛,位于F島的核電站,受9級特大地震影響医舆,放射性物質(zhì)發(fā)生泄漏俘侠。R本人自食惡果不足惜象缀,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望爷速。 院中可真熱鬧央星,春花似錦、人聲如沸遍希。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凿蒜。三九已至,卻和暖如春胁黑,著一層夾襖步出監(jiān)牢的瞬間废封,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工丧蘸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留漂洋,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓力喷,卻偏偏與公主長得像刽漂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子弟孟,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容