How to use an event bus in Android? Sample usages and ideas.

May be you are new to android or you have heard only of event buses like Otto and EventBus. I find that many people are still not using them in their projects mainly because they don’t find them useful or may be they don’t understand how they can use them. I will try to give some sample examples why and how you should use them and what are the advantages and disadvantages.

What can you do with them?

You can send events to all the components in your app which have not been destroyed by the GC. An event is a class which has an instance. You just pass that instance to the components listening for it.

The main idea and how to register for events?
The main idea is that you have something called “bus” to which all components can register. It is like your telephone central – if you want to get called you have to take a number from it. So all components will receive only the events they are looking for. I usually make a singleton instance containing the main bus. It looks something like this:

public class EventBusInstance {
    private Bus bus;
    private static EventBusInstance eventBusInstance;
    private EventBusInstance() {
        bus = new Bus();
    }

    public static EventBusInstance getInstance() {
        if(eventBusInstance == null) {
            eventBusInstance = new EventBusInstance();
        }

        return eventBusInstance;
    }

    public void register(Object object) {
        bus.register(object);
    }

    public void unregister(Object object) {
        bus.unregister(object);
    }

    public void post(Object event) {
        bus.post(event);
    }
}

It is something like a wrapper over the original one. I make this so I can easily replace the one I am using with another one. So if you want to listen for events the first thing you need to do is register/unregister using the bus. It is very important to unregister so you won’t cause any memory leaks or you won’t send events to components which don’t exist anymore. So if you have an activity the register/unregister actions look like this:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EventBusInstance.getInstance().register(this);
    }

    @Override
    protected void onDestroy() {
        EventBusInstance.getInstance().unregister(this);
        super.onDestroy();
    }

How do you register for a specific event?
After you have registered your component you have to create a method which declares what type of event it expects. It looks something like this:

    //Using Otto
    @Subscribe
    public void onUserLogout(UserLogoutEvent event) {
        supportInvalidateOptionsMenu();
    }

    //Using EventBus
    public void onEvent(UserLogoutEvent event) {
        supportInvalidateOptionsMenu();
    }

This method will get called when an event with the declared type “UserLogoutEvent” is posted. The two libraries available for an event bus implementation have 2 different approaches to declaring the methods. Otto works with annotations and don’t care for the method name. EventBus requires you to have onEvent method but with different event types expected.

How can you post events?

It is very simple. Just use the post method of the event bus and pass an instance of the event you want to share. You can also set data in that event and use it in the component which listens for it.

    @Override
    public void deliverResponse(AppsList response) {
        BusProvider.getInstance().post(new LoadAppsApiEvent(response));
    }

What choices of libraries you have?

There are two choices for event bus implementations at the moment and both are free and open-source – Otto and EventBus. What is the difference between them? The author of EventBus says Otto is slower. You can check the comparison HERE. EventBus requires you to have an onEvent method and Otto works with annotations. There are some other things you can check in the comparison document.

We spoke about how you can use the buses. Let’s talk now why and get some bigger ideas about them.

Why should you use them?

Events fit perfectly in the Android lifecycle. How? It is simple. When a component is detached or destroyed you just make sure you unregister it so it won’t receive any events. If it is attached or created – register it if you want it to receive events. Let me share you some use cases which are perfect for events:

  1. You can use events to notify UI changes to other components. You can post events onUserLogin or onUserLogout so all the components can have two states – for a logged in and for an anonymous user. You don’t have to communicate over the activity. Just: EventBus.getInstance().post()…
  2. You can share network responses using the event bus. You don’t need the isAdded() check any more. Because when a component is detached it just gets unregistered and it won’t receive any events. So you can pass your network data to the bus and only the active the components will receive it. Do you see where I am going? You don’t need callbacks, you can use the bus to send the network data. And you can have a separate ApiClient class or something like this which can just manage the network queries.
  3. Now imagine that you have an app displaying a list of apps. You have a vote button. Each app has a details page which again contains that vote button. The button displays the number of votes for the app. V00EyfLTb9U3vEoWAIbjrzSA_RcfR2A4hMmxcztNdvUDwjucU-9ynHzDEhAa_8VAUI8=h900 .So when the user presses the button the votes count updates. You don’t need to manage the code in two places. You can make a custom view class and register it for the events. Something like this:
    public class AppVoteButton extends LinearLayout {
        public static final String TAG = AppVoteButton.class.getSimpleName();
        private App baseApp;
        private LayoutInflater inflater;
    
        @InjectView(R.id.vote)
        TextView voteButton;
    
        public AppVoteButton(Context context) {
            super(context);
            if (!isInEditMode()) {
                init();
            }
        }
    
        public AppVoteButton(Context context, App baseApp) {
            super(context);
            this.baseApp = baseApp;
            if (!isInEditMode()) {
                init();
            }
        }
    
        public AppVoteButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            if (!isInEditMode()) {
                init();
            }
        }
    
        public AppVoteButton(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            if (!isInEditMode()) {
                init();
            }
        }
    
        @Override
        protected void onAttachedToWindow() {
            super.onAttachedToWindow();
            try {
                BusProvider.getInstance().register(this);
            } catch(Exception e) {
                e.printStackTrace();
                Crashlytics.logException(e);
            }
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            try {
                BusProvider.getInstance().unregister(this);
            } catch(Exception e) {
                Crashlytics.logException(e);
                e.printStackTrace();
            }
        }
    
        protected LayoutInflater getLayoutInflater() {
            if(inflater == null) {
                inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            }
    
            return inflater;
        }
    
        protected void init() {
            View view = getLayoutInflater().inflate(R.layout.view_vote_button, this, true);
            ButterKnife.inject(this, view);
            if(!shouldBeAbleToVote()) {
                voteButton.setText("0");
                return;
            }
    
            updateVoteButton();
        }
    
        protected void updateVoteButton() {
            if (hasVoted()) {
                voteButton.setTextColor(getResources().getColor(R.color.bg_secondary));
                voteButton.setBackgroundResource(R.drawable.btn_voted);
            } else {
                voteButton.setTextColor(getResources().getColor(R.color.bg_primary));
                voteButton.setBackgroundResource(R.drawable.btn_vote);
            }
            voteButton.setText(baseApp.getVotesCount());
        }
    
        public void setBaseApp(App baseApp) {
            this.baseApp = baseApp;
            updateVoteButton();
        }
    
        @OnClick(R.id.vote)
        public void vote(View view) {
            if(!LoginProviderFactory.get((Activity)getContext()).isUserLoggedIn()) {
                LoginUtils.showLoginFragment(getContext(), false, R.string.login_info_vote);
                return;
            }
    
            if(!shouldBeAbleToVote())
                return;
    
            if(hasVoted()) {
                downVote();
            } else {
                vote();
            }
        }
    
        protected boolean shouldBeAbleToVote() {
            return baseApp != null;
        }
    
        protected boolean hasVoted() {
            return baseApp.hasVoted();
        }
    
        protected void downVote() {
            ApiClient.getClient(getContext()).downVote(baseApp.getId(), SharedPreferencesHelper.getStringPreference(Constants.KEY_USER_ID));
        }
    
        protected void vote() {
            ApiClient.getClient(getContext()).vote(baseApp.getId(), SharedPreferencesHelper.getStringPreference(Constants.KEY_USER_ID));
        }
    
        @Subscribe
        public void onAppVote(AppVoteApiEvent event) {
            if(!baseApp.getId().equals(event.getVote().getAppId())) {
                return;
            }
            AppVote voteResult = event.getVote();
            baseApp.setVotesCount(voteResult.getVotes());
            baseApp.setHasVoted(event.isVote());
            voteButton.setText(voteResult.getVotes());
            updateVoteButton();
    
            postUserVotedEvent(event.isVote());
        }
    
        protected void postUserVotedEvent(boolean hasVoted) {
            BusProvider.getInstance().post(new AppVoteEvent(hasVoted));
        }
    }
    

    So you have now a reusable custom view. And when you vote from the app details page, that vote button in the list will also get updated. Nice, huh? And all you need is to place the button as xml in the layout and set the app in your code:

    <com.apphunt.app.ui.views.vote.AppVoteButton
                    android:id="@+id/btn_vote"
                    android:layout_width="47dp"
                    android:layout_height="47dp"
                    android:layout_alignParentRight="true"
                    android:gravity="right"/>
    AppVoteButton btnVote = (AppVoteButton) findViewById(R.id.btn_vote)
    btnVote.setBaseApp(app);
    

    Simple enough. But just keep in mind that all the existing vote buttons will get the AppVoteApiEvent. That’s why we need to check if the voted app id is for the button containing it. We want only the voted app button to update, not all buttons. This is a nice use case which shows you how you can extract the logic for voting in a custom component. You can think even bigger. Why not have classes which control the data using the event bus and the components just require the data from them? You can figure it out 🙂

I have given you some examples of how you can use the events mechanism in Android. Let’s see now the disadvantages.

What are the disadvantages?

The main disadvantage for me is that you cannot see where the event comes from. You have to check all the events in the project to understand what event you should subscribe to. This is a really big disadvantage. Suppose you make a new fragment and you want to receive when the user edits his profile. What is the event? Go and figure… This makes it hard for new developers to understand what is happening.

The second disadvantage comes with the android lifecycle and passing the network data as events (we use it in one of our projects). Let’s say when a network request finishes we post a SomeRequestApiEvent containing the response as data in the event. If you use fragments and they listen for these networks events suppose you have two fragments listening for the same network event. Suppose the second fragment is above the first one in the backstack and requires a new network request. So when the request finishes both the first and the second fragments are notified. This sometimes is bad, because you may not want the first fragment to update.
Why this happens? Because when you place a fragment in the backstack, the previous one does not get notified. It’s methods like: onDestroy or onPause are connected only with the activity, not with the fragment transactions. So it stays registered and is unregistered only when it is removed from the backstack.
What are the workarounds? You can manage the fragments registration/unregistration process by add an onBackstackChangeListener and use a custom BaseFragment class which all fragments extend which have register/unregister methods and you can call them from the activity. Not nice. There may be some other workarounds but for me the whole concept of having fragments listen for events is bad. I am still trying to figure it out with some alternative architecture like the Flux one. I also think that Mortar from Square is a good choice but I am in the process of figuring it out. I also prefer custom views over fragments.

These are the two main disadvantages. But if you figure out the correct architecture for the project you may find yourself playing in the land of “Wonderland”. 🙂

The End

I hope you got the big picture and you found some nice usages you can have in your project. Feel free to share, comment and suggest some cool ideas or architectures for this. 🙂

You may also like...