Android 中 EventBus 的使用(2):快取事件

Popo發表於2015-11-12

上一篇文章中,我曾提到我所選擇的是Green Robot提供的EventBus(Android平臺),而且這並非只是我一個人的選擇。在最近一次檢視中,我發現選擇它的人數已經是Otto(由Jake Wharton和其他大神們在Square上所提供的版本)的兩倍之多了。GR的版本顯然比Otto有更多的效能提升,但最令我動心的地方在於它新增了很多新功能。今天我就打算談談其中的一項:通過sticky事件進行事件快取。

sticky是什麼?

sticky事件就是指在EventBus內部被快取的那些事件。EventBus為每個類(class)型別儲存了最近一次被髮送的事件——sticky。後續被髮送過來的相同型別的sticky事件會自動替換之前快取的事件。當一個監聽者向EventBus進行註冊時,它可能會去請求這些快取事件。這時,所有已快取的事件就會立即自動傳送給這個監聽者,就象這些事件又重新剛被髮送了一次一樣。這就意味著,一個監聽者可以收到在它註冊之前就已經被髮送到EventBus中的事件(甚至是在這個監聽者的例項被建立出來前,這一點是不是很奇妙)。這一強大功能將有助於我們解決某些固有的問題,如Android上跨Activity和Fragment生命週期傳遞資料這種複雜問題,非同步呼叫等等。

使用sticky
使用sticky事件需要從兩個方面進行:

  • 一、傳送者必須通過呼叫bus.postSticky(event)將事件進行快取。
  • 二、監聽者須呼叫bus.registerSticky(this)以獲取快取的事件。

當呼叫了bus.registerSticky(this)後,監聽者會立即收到所有已在onEvent處理程式中定義過的那些已快取的事件。另外,監聽者也可以根據需要通過bus.getStickyEvent(SomeEvent.class)來獲取這些快取事件。

(注:呼叫postSticky,會像普通的post呼叫一樣將事件傳送給所有當前活動的監聽者,而不是僅限於那些通過registerSticky註冊的。registerSticky僅僅是使快取事件在註冊時被重發。)

sticky事件在快取中存在的時間並不確定。所以如果你想在某一時刻消除快取中的事件好讓它們不再被髮送,可以通過bus.removeStickyEvent(event)或bus.removeStickyEvent(SomeEvent.class),以及bus.removeAllStickyEvents()來實現。

令人厭惡的Bundles

我在上一篇文章中曾說過,我並不喜歡Android中的Bundle,而且儘量避免使用它們。我不喜歡被象Serializable或者是Parcelable這類物件所約束,尤其是它們還缺少對型別安全的檢查。我的意思是必竟這是Java,而不是Python或者Javascript什麼的。我希望我的IDE能夠發現並告訴我這樣的錯誤,如一個元件向另外一個元件傳送了一個不是它期望的物件型別。

不要誤會,Intent在程式間通訊時還是很有用的,在這種情況下將攜帶的資料序列化成通用格式是合情合理的。但如果僅僅是為了在使用者旋轉了一下螢幕後讓程式保持原來的狀態,以科學的名義說,有必要非得用這種方法麼?沒錯,我說的就是Android提供的處理配置改變的標準模式——在onSaveInstanceState(Bundle bundle)和onRestoreInstanceState(Bundle bundle)中儲存和恢復狀態資料。且不提那些荒唐複雜的Fragment生命週期問題,單單是保持執行狀態的這種處理方式就是我最不喜歡的Android開發特點之一。

Stinky Bundles Sticky Events

程式執行狀態除了儲存到Bundle中,另一種方法是將它們儲存到某些在配置改變時依然生存著的物件中去。GR的EventBus剛好內建了這種快取機制可供我們使用。

考慮下面這個響應“Master/Detail流程”的標準場景:

  • 有一個List元件(通常為Fragment)顯示一個摘要列表。
  • 另外一個元件(另一個Fragment)顯示每一項的詳細內容。
  • 點選某一列表項可顯示對應的詳細資訊。
  • 在豎屏模式中,列表和詳細資訊分為兩頁,各佔一屏,每次只能看到一個頁面。
  • 在橫屏模式中,列表在螢幕的左側,詳細資訊在右側,當左側的列表項被選中時,右側的詳細資訊也隨之改變。
  • 主Activity中包含一個佈局(layout)用於在不同模式下進行切換。

在這個例子中具有挑戰性的是當使用者在橫豎屏間來回切換時,程式要如何維護當前所選項的狀態。這個狀態的重要性不但在於詳細頁面需要知道該顯示哪條詳細資訊,而且列表也需要顯示出當前哪一項被選中了。此外,對於主頁面來說也需要知道當前是否有專案被選中,以便決定在豎屏模式時需要載入哪個頁面,列表或詳細資訊。

如你所見,這三個元件都需要同一個狀態資訊(被選中項)。使用傳統方法,這三個元件每一個都需要在各自的onSaveInstanceState方法中將這一狀態儲存進Bundle中,然後再從各自的onResumeInstanceState方法裡把資料取回來。不爽!

然而使用sticky事件,事情就變得簡單多了。為了更好地說明問題,我建立了一個Android示例工程:https://github.com/wongcain/EventBus-Config-Demo/ 下面所有的示例程式碼都包含在這個工程裡。

首先,建立一個事件類(ItemSelectedEvent.java)用於傳遞被選中項的位置資訊:

然後在List元件(ItemListFragment.java)的listItemClick方法裡傳送一個sticky事件:

接下來,Detail元件(ItemDetailFragment.java)註冊接收sticky事件,並定義一個ItemSelectedEvent的處理方法。當收到事件時,查詢並顯示被選中項的詳細資訊:

最後,在Main元件(MainActivity.java)中將所有內容集合到一起。Activity自身註冊監聽sticky事件,並建立與Detail元件一樣的ItemSelectedEvent處理方法。當收到事件時,根據當前頁面佈局(layout)決定將Detail fragment載入哪個合適的容器中。

注意,這個activity不僅監聽sticky事件,還傳送了另外一個sticky事件用來傳遞當前螢幕模式。這一事件隨後會被List fragment(ItemListFragment.java)收到,並且根據條件對列表進行設定:

另外可以看到,沒有一個元件要去實現onSaveInstanceState(Bundle bundle)以及onRestoreInstanceState(Bundle bundle)方法。取而代之的是它們只需簡單地依賴於在registerSticky(this)時自動傳送的快取事件。所以當使用者選擇一個專案並且在檢視詳細資訊時,以下情況便會在配置改變時自動發生:

  1. 在onPause時,每個元件都會將自身從EventBus登出掉。
  2. Main activity重啟並在它的onResume方法裡註冊監聽sticky事件。
  3. 快取的ItemSelectedEvent被髮送到Main activity,然後Detail fragment被載入。
  4. Detail fragment的onResume被呼叫並且接收到ItemSelectedEvent,從而使得被選中專案的詳細資訊被顯示出來。
  5. 此外,List fragment的onResume被呼叫並且收到ItemSelectedEvent和LayoutEvent,然後根據當前佈局正確地顯示被選中專案。

希望這篇文章對你能有幫助。如之前提到的,所有的示例程式碼都可以在這裡訪問到:https://github.com/wongcain/EventBus-Config-Demo/

下一篇將是有關EventBus系列教程的最後一篇, 我將談一談在EventBus中有關跨越多執行緒和程式的有關內容。

相關文章