Earlier in the book, we covered the concept of an event bus as a
way of communicating between portions of our app, focusing on one event bus
implementation: greenrobot’s EventBus. Later, in the chapter on broadcast
Intent
objects, we briefly covered LocalBroadcastManager
.
However, those are not the only event buses available for Android, and others may fit your needs better. In this chapter, we will explore these and other event bus implementations, to compare and contrast.
Understanding this chapter requires that you have read the core chapters
of this book, particularly the chapters on
basic event bus usage, broadcast Intents
,
AlarmManager
and the scheduled service pattern, and Notifications
.
The sample apps in this chapter are generally designed to run forever.
It is unlikely that you really want them to run forever, though. Hence, please uninstall each sample after experimenting with it, particularly if you are testing on hardware, such as your personal phone. Your battery will appreciate it.
You can think of the standard Intent
and <intent-filter>
system as a three-channel
event bus:
The component starting an activity does not need to communicate directly with code for that activity — in fact, often times this is impossible, as they are separate apps running in separate processes. Instead, the component starting an activity sends an event indicating the particular operation to be performed (e.g., view this URL), and Android and the user determine which of candidate consumers is the one to process that event.
However, broadcast Intent
objects are a closer analogue to a real “event bus”, in that
an event produced by somebody can be consumed by zero, one, or several subscribed
consumers, based upon the filtering provided by <intent-filter>
elements in
the manifest or IntentFilter
objects for use with registerReceiver()
.
In theory, you could use broadcast Intent
objects as the backbone for a fairly flexible
event bus within your app. In practice, this is not usually a good idea:
However, if you specifically need a cross-process event bus, such as between
a suite of related apps, using a broadcast Intent
is a very likely choice.
As was briefly noted earlier in the book, the Android
Support package offers a LocalBroadcastManager
. This is designed to offer an
event bus with a feel very similar to classic broadcast Intent
objects, but local to
your process. Not only does this avoid IPC overhead, but it improves security,
as other apps have no means of spying on your internal communications.
LocalBroadcastManager
is supplied by both the support-v4
and
support-v13
libraries. Generally speaking, if your minSdkVersion
is less than 13, you probably should choose support-v4
.
Let’s see LocalBroadcastManager
in action via the
Intents/Local
sample project.
Here, our LocalActivity
sends a command to a NoticeService
from
onCreate()
:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
notice=(TextView)findViewById(R.id.notice);
startService(new Intent(this, NoticeService.class));
}
The NoticeService
simply delays five seconds, then sends a local
broadcast using LocalBroadcastManager
:
package com.commonsware.android.localcast;
import android.app.IntentService;
import android.content.Intent;
import android.os.SystemClock;
import android.support.v4.content.LocalBroadcastManager;
public class NoticeService extends IntentService {
public static final String BROADCAST=
"com.commonsware.android.localcast.NoticeService.BROADCAST";
private static Intent broadcast=new Intent(BROADCAST);
public NoticeService() {
super("NoticeService");
}
@Override
protected void onHandleIntent(Intent intent) {
SystemClock.sleep(5000);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}
}
Specifically, you get at your process’ singleton instance of
LocalBroadcastManager
by calling getInstance()
on the
LocalBroadcastManager
class.
Our LocalActivity
registers for this local broadcast in
onStart()
, once again using getInstance()
on
LocalBroadcastManager
:
@Override
public void onStart() {
super.onStart();
IntentFilter filter=new IntentFilter(NoticeService.BROADCAST);
LocalBroadcastManager.getInstance(this).registerReceiver(onNotice,
filter);
}
LocalActivity
unregisters for this broadcast in onStop()
:
@Override
public void onStop() {
super.onStop();
LocalBroadcastManager.getInstance(this).unregisterReceiver(onNotice);
}
The BroadcastReceiver
simply updates a TextView
with the current
date and time:
private BroadcastReceiver onNotice=new BroadcastReceiver() {
public void onReceive(Context ctxt, Intent i) {
notice.setText(new Date().toString());
}
};
If you start up this activity, you will see a “(waiting...)
” bit of
placeholder text for about five seconds, before having that be
replaced by the current date and time.
The BroadcastReceiver
, the IntentFilter
, and the Intent
being
broadcast are the same as we would use with full broadcasts. It is
merely how we are using them — via LocalBroadcastManager
– that dictates they are local to our process versus the
standard device-wide broadcasts.
That sample is not terribly realistic, but it is simple.
A somewhat more realistic sample is the one using AlarmManager
and JobIntentService
from
elsewhere in the book. However, that app is also fairly unrealistic,
at least in terms of its output, as LogCat is not very useful to users. A more typical
approach for a background service like this is to notify a foreground Activity
, if there
is one, about work that was accomplished, and otherwise display a Notification
.
We described that pattern in the chapter on Notifications
.
In the
EventBus/LocalBroadcastManager
sample project, we blend:
Activity
or a Notification
LocalBroadcastManager
to keep the communications in-processThe EventDemoActivity
that is our app’s entry point is a bit similar to the one used in the
AlarmManager
demo, in that it calls scheduleAlarms()
on PollReceiver
to set up the
AlarmManager
schedule:
package com.commonsware.android.eventbus.lbm;
import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
public class EventDemoActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content,
new EventLogFragment()).commit();
PollReceiver.scheduleAlarms(this);
}
}
}
However, we also put an EventLogFragment
on the screen, if it is not already there, via
a FragmentTransaction
. This is where we will display events coming from the service, while
our activity is in the foreground. We will examine EventLogFragment
and how it participates
in the event bus shortly.
PollReceiver
is largely unchanged from its AlarmManager
demo original edition. This
BroadcastReceiver
will be used both for getting control at boot time (to reschedule the alarms,
wiped on the reboot) and for sending the work to the ScheduledService
for processing:
package com.commonsware.android.eventbus.lbm;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
public class PollReceiver extends BroadcastReceiver {
private static final int PERIOD=60000; // 1 minute
private static final int INITIAL_DELAY=5000; // 5 seconds
@Override
public void onReceive(Context ctxt, Intent i) {
if (i.getAction() == null) {
ScheduledService.enqueueWork(ctxt);
}
else {
scheduleAlarms(ctxt);
}
}
static void scheduleAlarms(Context ctxt) {
AlarmManager mgr=
(AlarmManager)ctxt.getSystemService(Context.ALARM_SERVICE);
Intent i=new Intent(ctxt, PollReceiver.class);
PendingIntent pi=PendingIntent.getBroadcast(ctxt, 0, i, 0);
mgr.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + INITIAL_DELAY,
PERIOD, pi);
}
}
Before, our ScheduledService
just dumped a message to LogCat. This was crude but effective
for what that demo required. Now, we want our service to let the UI layer know about
some work that was accomplished, or to raise a Notification
.
In this case, the “work” is generating a random number.
package com.commonsware.android.eventbus.lbm;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.v4.app.JobIntentService;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import java.util.Calendar;
import java.util.Random;
public class ScheduledService extends JobIntentService {
private static int NOTIFY_ID=1337;
private static final int UNIQUE_JOB_ID=1337;
private static final String CHANNEL_WHATEVER="channel_whatever";
private Random rng=new Random();
static void enqueueWork(Context ctxt) {
enqueueWork(ctxt, ScheduledService.class, UNIQUE_JOB_ID,
new Intent(ctxt, ScheduledService.class));
}
@Override
public void onHandleWork(Intent i) {
Intent event=new Intent(EventLogFragment.ACTION_EVENT);
long now=Calendar.getInstance().getTimeInMillis();
int random=rng.nextInt();
event.putExtra(EventLogFragment.EXTRA_RANDOM, random);
event.putExtra(EventLogFragment.EXTRA_TIME, now);
if (!LocalBroadcastManager.getInstance(this).sendBroadcast(event)) {
NotificationManager mgr=
(NotificationManager)getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O &&
mgr.getNotificationChannel(CHANNEL_WHATEVER)==null) {
mgr.createNotificationChannel(new NotificationChannel(CHANNEL_WHATEVER,
"Whatever", NotificationManager.IMPORTANCE_DEFAULT));
}
NotificationCompat.Builder b=new NotificationCompat.Builder(this, CHANNEL_WHATEVER);
Intent ui=new Intent(this, EventDemoActivity.class);
b.setAutoCancel(true).setDefaults(Notification.DEFAULT_SOUND)
.setContentTitle(getString(R.string.notif_title))
.setContentText(Integer.toHexString(random))
.setSmallIcon(android.R.drawable.stat_notify_more)
.setTicker(getString(R.string.notif_title))
.setContentIntent(PendingIntent.getActivity(this, 0, ui, 0));
mgr.notify(NOTIFY_ID, b.build());
}
}
}
LocalBroadcastManager
, as we have seen, uses the same Intent
and IntentFilter
and
BroadcastReceiver
structures as are used with regular broadcasts, just via a singleton
message bus (LocalBroadcastManager.getInstance()
) instead of the framework’s IPC engine.
Hence, we need an Intent
that represents the message, so we create one, using an action
string published by the EventLogFragment
. We also attach two extras to this Intent
, using
keys published by EventLogFragment
: the random number, plus the time of this event.
We then call sendBroadcast()
on the singleton LocalBroadcastManager
. This returns a
boolean
value, true
indicating that one or more locally-registered receivers were delivered
the Intent
, false
otherwise. Hence, if sendBroadcast()
returns true
, we can assume
that somebody in the UI layer picked up our message and is now responsible for displaying
these results to the user.
Conversely, if sendBroadcast()
returns false
, we must assume that the UI layer did
not receive the message, and so the service should inform the user directly, in this case
via a Notification
, showing the random number as the text in the notification drawer.
EventLogFragment
, therefore, is responsible for:
In this case, we use a retained ListFragment
with a ListView
set into transcript mode,
meaning that entries are added at the bottom, and older entries scroll off the top, like a
chat transcript:
package com.commonsware.android.eventbus.lbm;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ListFragment;
import android.support.v4.content.LocalBroadcastManager;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
public class EventLogFragment extends ListFragment {
static final String EXTRA_RANDOM="r";
static final String EXTRA_TIME="t";
static final String ACTION_EVENT="e";
private EventLogAdapter adapter=null;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onViewCreated(@NonNull View view,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getListView().setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
if (adapter == null) {
adapter=new EventLogAdapter();
}
setListAdapter(adapter);
}
@Override
public void onStart() {
super.onStart();
IntentFilter filter=new IntentFilter(ACTION_EVENT);
LocalBroadcastManager.getInstance(getActivity())
.registerReceiver(onEvent, filter);
}
@Override
public void onStop() {
LocalBroadcastManager.getInstance(getActivity())
.unregisterReceiver(onEvent);
super.onStop();
}
class EventLogAdapter extends ArrayAdapter<Intent> {
DateFormat fmt=new SimpleDateFormat("HH:mm:ss", Locale.US);
public EventLogAdapter() {
super(getActivity(), android.R.layout.simple_list_item_1,
new ArrayList<Intent>());
}
@SuppressLint("DefaultLocale")
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView row=
(TextView)super.getView(position, convertView, parent);
Intent event=getItem(position);
Date date=new Date(event.getLongExtra(EXTRA_TIME, 0));
row.setText(String.format("%s = %x", fmt.format(date),
event.getIntExtra(EXTRA_RANDOM, -1)));
return(row);
}
}
private BroadcastReceiver onEvent=new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
adapter.add(intent);
}
};
}
The ListAdapter
for the ListView
is an EventLogAdapter
, an ArrayAdapter
for Intent
objects, where in getView()
we populate the list rows with the time and random
value.
In onStart()
and onStop()
, we register for (and unregister from) the desired broadcast,
pointing to an onEvent
BroadcastReceiver
that adds the incoming Intent
to the
EventLogAdapter
. That, in turn, updates the ListView
.
The result is that while the activity is in the foreground, the events will be displayed to the user directly:
Figure 646: LocalBroadcastManager as Event Bus, Demo Activity
Whereas if events are processed while the activity is not in the foreground, a
Notification
will be shown with the last results:
Figure 647: LocalBroadcastManager as Event Bus, Demo Notification
When you send a “real” broadcast Intent
, your Intent
is converted
into a byte array (courtesy of the Parcelable
interface) and
transmitted to other processes. This occurs even if the recipient of
the Intent
is within your own process — that is what makes
LocalBroadcastManager
faster, as it avoids the inter-process
communication.
However, since LocalBroadcastManager
does not need to send your
Intent
between processes, that means it does not turn your Intent
into a byte array. Instead, it just passes the Intent
along to any
registered BroadcastReceiver
with a matching IntentFilter
. In
effect, while “real” broadcasts are pass-by-value, local broadcasts
are pass-by-reference.
This can have subtle side effects.
For example, there are a few ways that you can put a collection into
an Intent
extra, such as putStringArrayListExtra()
. This takes an
ArrayList
as a parameter. With a real broadcast, once you send the
broadcast, it does not matter what happens to the original
ArrayList
— the rest of the system is working off of a copy.
With a local broadcast, though, the Intent
holds onto the
ArrayList
you supplied via the setter. If you change that
ArrayList
elsewhere (e.g., clear it for reuse), the recipient of
the Intent
will see those changes.
Similarly, if you put a Parcelable
object in an extra, the Intent
holds onto the actual object while it is being broadcast locally,
whereas a real broadcast would have resulted in a copy. If you change
the object while the broadcast is in progress, the recipient of the
broadcast will see those changes.
This can be a feature, not a bug, when used properly. But, regardless, it is a non-trivial difference, one that you will need to keep in mind.
While LocalBroadcastManager
is certainly useful, it has some
serious limitations.
The biggest is that it is purely local. While traditional broadcasts
can either be internal (via setPackage()
) or device-wide,
LocalBroadcastManager
only handles the local case. Hence, anything
that might involve other processes, such as a PendingIntent
, will
not use LocalBroadcastManager
. For example, you cannot register a
receiver through LocalBroadcastManager
, then use a getBroadcast()
PendingIntent
to try to reach that BroadcastReceiver
. The
PendingIntent
will use the regular broadcast Intent
mechanism,
which the local-only receiver will not respond to.
Similarly, since a manifest-registered BroadcastReceiver
is spawned
via the operating system upon receipt of a matching true broadcast,
you cannot use such receivers with LocalBroadcastManager
. Only a
BroadcastReceiver
registered via registerReceiver()
on the
LocalBroadcastManager
will use the LocalBroadcastManager
.
Also, LocalBroadcastManager
does not offer ordered or sticky
broadcasts.
LocalBroadcastManager
has two major advantages:
However, that same dependency on the Intent
and IntentFilter
structure
adds bulk and limits flexibility. Hence, it is not surprising that there
are alternative event buses to LocalBroadcastManager
.
Java, outside of Android, has had a few event bus implementations. One of the more popular ones in recent years has been the event bus that is part of Google’s Guava family of libraries. However, while a Java event bus perhaps can be used on Android, it may not be optimal for Android. Hence, a few projects have started with Guava’s event bus implementation and have extended it to be a bit more Android-aware, or perhaps even Android-centric.
greenrobot’s EventBus is one such event bus.
NOTE: For the purposes of this chapter, “greenrobot’s EventBus”
refers to the library, and “EventBus
”" refers to the EventBus
Java
class in that library.
With LocalBroadcastManager
, you work with a singleton instance, calling methods
like registerReceiver()
and sendBroadcast()
upon it to subscribe to and raise
events, respectively.
With greenrobot’s EventBus, you work with an EventBus
instance, calling methods like
register()
and post()
upon it to subscribe to and raise
events, respectively. Usually, we use the singleton instance of EventBus
that we get by calling getDefault()
on the EventBus
class, but you
are welcome to have different EventBus
objects, representing distinct
communications channels, if you wish.
Hence, at the core, greenrobot’s EventBus behaves much like LocalBroadcastManager
. What
differs is in the nature of the events and the subscribers.
With LocalBroadcastManager
,
events are Intent
objects. With greenrobot’s EventBus, an event can be whatever data type you like.
Hence, you can create your own ...Event
classes, holding whatever bits of
data, in whatever data types suit you — you are not restricted to things that
can go in an Intent
extra. However, as has been noted on occasion,
“with great power comes great responsibility”,
and so you will need to ensure that you use this carefully and do not
wind up creating some sort of memory leak as a result. For example,
do not pass something from an Activity
to a Service
via a custom
event, where the Service
will hold onto that information for a long
time, if that “something” holds a reference back to the Activity
.
With LocalBroadcastManager
, subscribers are BroadcastReceiver
s, who use
an IntentFilter
to identify which events they are interested in. With greenrobot’s EventBus,
subscribers are any class you want. A special @Subscribe
annotation is used
to both indicate what sorts of events the subscriber is interested in
(based on the parameter to the annotated method) and what method should be
invoked when a matching event is raised (the annotated method itself).
Hence, not only do you use custom event classes to allow you to carry
along custom data, but you use them as a filtering mechanism, much like
you would use custom action strings with LocalBroadcastManager
.
To see how this works, take a look at the
EventBus/GreenRobot3
sample project, which is a clone of the EventBus/LocalBroadcastManager
demo,
but one where we substitute in greenrobot’s EventBus as a replacement for LocalBroadcastManager
.
Our activity and PollReceiver
are unchanged: they did not directly interact
with LocalBroadcastManager
and do not need to interact with greenrobot’s EventBus. The
changes are isolated in our ScheduledService
and EventLogFragment
.
We will need an EventBus
instance, one that serves the same basic
role as does the singleton LocalBroadcastManager
retrieved by getInstance()
.
As noted above, you can call getDefault()
on EventBus
to get a
singleton EventBus
instance, and this suffices in most cases.
When it comes time for us to send a message, we can call post()
on
the EventBus
, supplying whatever sort of event object that we want:
EventBus.getDefault().post(randomEvent);
Here, we are posting an instance of a RandomEvent
:
package com.commonsware.android.eventbus.greenrobot;
import java.util.Calendar;
import java.util.Date;
public class RandomEvent {
Date when=Calendar.getInstance().getTime();
int value;
RandomEvent(int value) {
this.value=value;
}
}
Over in our EventLogFragment
, rather than register and unregister a
BroadcastReceiver
in onStart()
and onStop()
, we register and unregister
the fragment itself with the EventBus
:
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
EventBus.getDefault().unregister(this);
super.onStop();
}
Now, we can use the @Subscribe
annotation to arrange to receive any event
we want that is delivered via this EventBus
, based on event class. Since we want
to receive RandomEvent
messages, we merely need to have a public void
method, taking a RandomEvent
parameter, marked with the @Subscribe
annotation,
such as onRandomEvent()
:
@Subscribe(threadMode = ThreadMode.MAIN)
public void onRandomEvent(final RandomEvent event) {
adapter.add(event);
}
Note that the method name can be anything we want, as it is the annotation, not the method name, that identifies this as being an event handling method.
Since Java annotations can take key-value pairs for configuration,
EventBus 3.x uses that to configure the behavior of @Subscribe
. Here,
we use @Subscribe(threadMode = ThreadMode.MAIN)
, to indicate that we want
this event to be delivered to this method on the main application thread.
In this method, we can do what we need to with our RandomEvent
. In our case,
EventLogAdapter
has been modified to be an ArrayAdapter
of RandomEvent
,
as opposed to being an ArrayAdapter
of Intent
as in the earlier sample.
What we want to do is append the new RandomEvent
to the end of the adapter.
What is missing, though, is the logic we used in LocalBroadcastManager
to
determine if somebody received our message, where we raised a Notification
if that is not the case.
The solution for this with greenrobot’s EventBus is to call
hasSubscriberForEvent()
, with the Java Class
object of the event that we would
like to post()
. If this returns true
, we have a current subscriber; otherwise,
we do not.
So, the full onHandleWork()
implementation uses hasSubscriberForEvent()
and
either uses post()
to raise the event or displays a Notification
itself:
@Override
public void onHandleWork(Intent i) {
RandomEvent randomEvent=new RandomEvent(rng.nextInt());
if (EventBus.getDefault().hasSubscriberForEvent(randomEvent.getClass())) {
EventBus.getDefault().post(randomEvent);
}
else {
NotificationManager mgr=
(NotificationManager)getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O &&
mgr.getNotificationChannel(CHANNEL_WHATEVER)==null) {
mgr.createNotificationChannel(new NotificationChannel(CHANNEL_WHATEVER,
"Whatever", NotificationManager.IMPORTANCE_DEFAULT));
}
NotificationCompat.Builder b=new NotificationCompat.Builder(this, CHANNEL_WHATEVER);
Intent ui=new Intent(this, EventDemoActivity.class);
b.setAutoCancel(true).setDefaults(Notification.DEFAULT_SOUND)
.setContentTitle(getString(R.string.notif_title))
.setContentText(Integer.toHexString(randomEvent.value))
.setSmallIcon(android.R.drawable.stat_notify_more)
.setTicker(getString(R.string.notif_title))
.setContentIntent(PendingIntent.getActivity(this, 0, ui, 0));
mgr.notify(NOTIFY_ID, b.build());
}
}
There is a race condition, though. Since our hasSubscriberForEvent()
call
is happening on a background thread, it is possible that between the
hasSubscriberForEvent()
call and the post()
call that the subscriber
unsubscribes. This is an unlikely occurrence here, but it is worth keeping in mind.
In addition to the threading features, greenrobot’s EventBus has a few other noteworthy bells and whistles:
ThreadMode.POSTING
(events are delivered on the same thread they are posted from)
and ThreadMode.BACKGROUND
(events are delivered on a background
thread, with EventBus
using its own thread if the event was
posted from the main application thread).postSticky()
and registerSticky()
allow you to have sticky events,
much like sticky broadcasts with the classic broadcast Intent
system.@Subscribe(priority = 1)
). If a higher-priority handler
wants to consume the event, it can call cancelEventDelivery()
on
the EventBus
, passing in the event object.For a few years, a third major event bus implementation was popular:
Square’s Otto. Like greenrobot’s EventBus, Otto was based off of Guava’s
EventBus
class and was tuned towards Android app development. It shared
some characteristics with greenrobot’s EventBus, owing to the shared
heritage. On the whole, greenrobot’s EventBus was a bit more complex to
use but offered greater flexibility.
Square has since discontinued work on Otto, so unless you have existing legacy code that uses Otto, you should use some other event bus implementation.