Using an EventBus in Android Pt 2: Sticking Your Config

Originally published January 29, 2015.

As I mentioned in my previous post, my EventBus implementation of choice for Android is Green Robot’s EventBus, and I am not alone. At last check it has been forked about twice as many times as Otto, the EventBus implementation by Jake Wharton and other Android rockstars over at Square. While GR’s EventBus apparently has some significant performance improvements over Otto, what really sells it for me are the out-of-the-box add-on features that you get with it. Today I want to talk about one of those: event caching with “sticky” events.

What is this sticky stuff?

Sticky events are events that are maintained in an internal cache by the EventBus. The bus maintains the most recent 1 event of each type (class) that is posted as sticky. A subsequent sticky post of an event of the same type replaces the previous event in the cache. When a subscriber registers on the bus, it may request sticky events. When this happens (and this is where the magic is) any cached sticky events are immediately, automatically delivered to the subscriber as if they were re-posted on the bus. This means that subscriber instances can receive events that were posted before the subscriber registered on the bus (or even before the subscribing class was even instantiated). This can be a powerful tool for getting around some of the inherant problems that you run into when passing around state on Android given the complexity of Activity and Fragment lifecycles, asynchronous calls, etc.

Get sticky with it

There are 2 different aspects to using sticky events:

  • First, the publisher of the event must specifically request that a event be cached by calling the bus.postSticky(event) method.
  • Secondly, the subscriber must request to receive cached events by registering via bus.registerSticky(this).

Upon calling bus.registerSticky(this) the subscriber immediately receives any cached events for which they have defined an onEvent handler. Alternately, a subscriber may request cached events on-demand via bus.getStickyEvent(SomeEvent.class).

(Note that all actively registered subscribers receive events posted via postSticky at the time of it being posted, just as they do with the standard post, regardless of whether or not they registered with registerSticky. Using registerSticky simply causes cached events to automatically be re-delivered at registration time.)

Sticky events remain in the cache indefinitely. So if you want an event to be cleared from the cache at some point so that it is no longer fired, you can call bus.removeStickyEvent(event) or bus.removeStickyEvent(SomeEvent.class) or bus. removeAllStickyEvents() to clean up stale events.

Stinky Bundles

As may be evident from my previous post, I am not a fan of Android Bundles, and avoid using them whenever possible. I don’t like being limited to Serializable or worse yet Parcelable objects, and I especially don’t like the lack of type-safety involved. I mean this is Java after all, not Python or Javascript. I expect my IDE to be able to able to tell me if one of my components is sending a different type of object than another component is expecting to receive.

Don’t get me wrong… using Intents to communicate across applications is very handy, and in that scenario it makes sense that the payload would be serialized into some generic form. But why, for the love of science, should I need to go through this hassle just to retain state after a user rotates his device?! I’m speaking of course of the standard (#icky) pattern of handling configuration changes, where you need to save and retrieve your state using the (#gross) methods onSaveInstanceState(Bundle bundle) and onRestoreInstanceState(Bundle bundle). Aside from the absurdly (#unneccessary) complex lifecycle of Fragments, dealing with runtime state persistence is my least favorite aspect of Android development.

Sticky Events

The alternative to stashing your runtime state in a Bundle is to stash it elsewhere, to cache it in some other object whose lifecycle survives the configuration change. And since GR’s EventBus has a built-in mechanism for caching, we can use it to do just that.

Consider the standard, responsive “Master/Detail Flow” scenario:

  • One List component (typically a Fragment) displays a list with summary information for each item.
  • Another Detail component (another Fragment) that displays the full details of a single item.
  • Clicking an item in the list displays the details of the respective selected item.
  • In portrait mode, each the list and details fill the whole screen, and only one of the two is shown any given time.
  • In landscape mode, the list remains displaying on the left, and the details change on the right as different items are selected from the list.
  • A Main component (Activity) contains a layout that switches between a one- and two-pane display.

The challenge that I want to address here here is how to retain the state of which (if any) item is currently selected when the user switches between the portrait and landscape mode. This state is important not only to the Detail which obviously needs to know which item’s details to display, but also the List which needs to visually identify which item is currently selected. Also, it is important for Main to know if there is an item selected or not, so that it knows whether or not to load Detail or List in portrait mode.

As you can see, the same state information (which item is selected) is needed for all 3 of our components. Using the traditional method, each of the 3 components would need have their onSaveInstanceState methods implemented set this information into a bundle, and their onResumeInstanceState methods implemented to extract it back out again. Yuck.

But using sticky events, the solution is much simpler. To illustrate the solution, I have created and posted a working Android Studio project here: https://github.com/wongcain/EventBus-Config-Demo/ All of the code examples that follow are contained in this project.

First, an event class (ItemSelectedEvent.java) is created to carry the position of the selected item:

public class ItemSelectedEvent {  
public final int position;
public ItemSelectedEvent(int position) {
this.position = position;
}
}

Then, in the List component (ItemListFragment.java), I extend the list item click handler to post a sticky event:

@Override
public void onListItemClick(ListView listView, View itemView, int position, long id) {
super.onListItemClick(listView, itemView, position, id);
bus.postSticky(new ItemSelectedEvent(position));
}

Next, the Detail component (ItemDetailFragment.java) registers to receive sticky events, and defines a handler for ItemSelectedEvent. Upon receiving the event, the handler queries and displays the respective item’s details:

    @Override
public void onResume() {
super.onResume();
bus.registerSticky(this);
}

@Override
public void onPause() {
bus.unregister(this);
super.onPause();
}

...

public void onEvent(ItemSelectedEvent event) {
Item item = MockDataSource.ITEMS.get(event.position);
titleView.setText(item.title);
dateView.setText(item.getDateStr());
bodyView.setText(item.body);
}

Finally, its all tied together in the Main component (MainActivity.java). The activity registers itself to receive sticky events, and creates a handler for the same ItemSelectedEvent as does the Detail fragment. Upon receiving the event, the Detail fragment is loaded into the appropriate container view, depending on the current layout:

    @Override
protected void onResume() {
super.onResume();
bus.registerSticky(this);
bus.postSticky(new LayoutEvent(isTwoPane()));
}

@Override
protected void onPause() {
bus.unregister(this);
super.onPause();
}

public void onEvent(ItemSelectedEvent event) {
if(isTwoPane()){
getFragmentManager().beginTransaction()
.replace(detailContainer.getId(), new ItemDetailFragment())
.commit();
} else {
getFragmentManager().beginTransaction()
.replace(listContainer.getId(), new ItemDetailFragment())
.addToBackStack(ItemDetailFragment.class.getName())
.commit();
}
}

Notice that this activity not only listens for sticky events, but it also posts a different sticky event to communicate out the portait/landscape layout mode. This event is in turn received by our List fragment (ItemListFragment.java) to conditionally configure the display of the selected list item:

    public void onEvent(LayoutEvent event) {
if(event.isTwoPane){
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
updateSelectedItem(activePosition);
} else {
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
}
}

Also notice that none of the components implement onSaveInstanceState(Bundle bundle) and/or onRestoreInstanceState(Bundle bundle). Instead they simply rely on the automatic delivery of cached events that occurs on registerSticky(this). So, given that a user has selected an item and is viewing the item’s details, the following occurs on automatically configuration change:

  1. Each component onPause is called where the component unregisters itself from the bus.
  2. The Main activity is restarted, calling it’s onResume event which registers it for sticky events on the bus.
  3. The cached ItemSelectedEvent is delivered to the Main activity, which causes it to load the Detail fragment.
  4. The Detail fragment’s onResume is called and the ItemSelectedEvent is delivered causing it to display the selected item’s details.
  5. Additionally, the List fragment’s onResume is called, and the ItemSelectedEvent and LayoutEvent are delivered so that the currently selected item can be visualized as is appropriate for the current layout.

So what did we learn today?

In summary,

Good:

Bad:

Hopefully this has been helpful. As mentioned before, all of the code for this example is available here: https://github.com/wongcain/EventBus-Config-Demo/

Next time I will complete my series on Using an Event Bus with a discussion about using a bus across multiple threads and processes.


The original Disqus thread for this article can be found here.