Messaging with LocalBroadcastManager

I guess every Android developer had at some point bumped into a callback hell, where you have to propagate some callback interface through thousands of objects in order to be able to do something when a user clicks on some UI element. And it gets even worse when you for example make it work across configuration changes such as orientation change. I had a similar pain a couple of weeks ago and had to come up with some solution to do the job without polluting all my UI components with huge amount of boilerplate.

To give you an idea of the scale of the problem, I will start with an example architecture of a typical UI. Imagine you have an Activity, which has a ViewPager containing several ListFragments generated by a ViewPagerAdapter. For each of these ListsFragments, there is a ListAdapter which builds the necessary views contained in the list. Everything is clean and looks sweet and you are feeling proud of yourself. Now, imagine in some of these views there is a Button which has to somehow communicate up to the Activity level something… Now if you do it directly with some callback interface, you will have to propagate it from the Activity through the ViewPagerAdapter -> ListFragment -> ListAdapter -> View, which is just crazy. Such situations are really rare, and actually if you encounter them too often, then maybe you should take a second look at your design and how your UI is being built.

A simple callback was definitely not a solution in my case as well, as I could see it destroying my clean code and turning it into a monster which nobody would be able to understand. So I looked at alternative ways of enabling UI components to talk to each other. Thankfully, the Android framework offers enough ways to communicate between different application components. There are also third party libraries which implement additional solutions such as event buses. I found two such libraries - Otto and the Greenrobot EventBus - both of which I definitely recommend. However, they offered more than I really needed so I have decided to implement something simpler based on two Android components - Intents and BroadcastReceivers.

Solution with Intents and BroadcastReceivers

The solution presented in the following sections is based on the idea of broadcasting Intents for specific actions containing a payload carried in the Intents’ extras. So when one component needs to tell something to another, it just fires a broadcast Intent for an Action which the other component has registered BroadcastReceiver for. Now, I didn’t want to tell the whole system that a button was clicked in one of the lists in my application because of security, privacy and performance reasons. Therefore, I have decided to use a nice component from the support library called LocalBroadcastManager. According to the official documentation it provides the following nice features:

  • You know that the data you are broadcasting won’t leave your app, so don’t need to worry about leaking private data.
  • It is not possible for other applications to send these broadcasts to your app, so you don’t need to worry about having security holes they can exploit.
  • It is more efficient than sending a global broadcast through the system.

Designing the LocalMessenger

Although the LocalBroadcastManager has a really nice API and one could just create one BroadcastReceiver and call a couple of methods to make the whole thing work, I have decided to encapsulate all of it into a single class. I had a couple of goals when I was designing it:

  1. Has to be accessible from everywhere and have a consistent state
  2. Has to provide simple methods for broadcasting some payload for a specific Action
  3. Has to encapsulate all BroadcastReceiver specific stuff and require listeners who register to implement just a simple interface

The first one is easy, I have decided to implement the LocalMessenger using the Singleton creational pattern. The LocalBroadcastManager itself is also a Singleton, so this pattern works perfectly for the wrapper as well.

The second one, comes almost out of the box thanks to the LocalBroadcastManager API. I have only encapsulated the Intent object creation so that clients of the LocalMessenger only need to define an action and a payload they want to send. The heavy lifting is done by the LocalBroadcastManager and these two methods for firing Broadcasts:

  • public boolean sendBroadcast (Intent intent) - This method fires a broadcast with the Intent and returns immediately. It returns false if there is no BroadcastReceiver registered for this Intent’s Action
  • public void sendBroadcastSync (Intent intent) - This method fires a broadcast with the Intent and blocks until its BroadcastReceiver is executed. If there is no receiver, then it returns immediately.

The third one is a little bit tricky. I have decided to use a simple listener interface with a single callback method containing just the message payload which app components need to implement in order to receive messages. This way they don’t have to build and manage their own BroadcastReceiver object instances or understand what Intents are. Instead, these instances are built and managed inside the LocalMessenger. The app components which want to listen for some message just need to register for a specific action and implement the provided interface.

public interface OnReceiveBroadcastListener {
    public void onReceiveBroadcast(Bundle extras);
}

Although this approach is clean, it introduces a possible memory leak. It is always a bad idea to keep hard references to stuff inside a singleton which has longer life than for example an Activity - think about orientation changes. So just to make the LocalMessenger extra safe, I have decided to use WeakReference to deal with this problem. In addition, WeakReferences help managing the encapsulated BroadcastReceivers better, i.e. when a registered listener object has been garbage collected, we can also internally unregister and free the corresponding BroadcastReceiver object encapsulated inside the LocalMessenger.

The final result

/**
 * <p>
 * This class offers a simple messaging center through which various app 
 * components can send and receive simple messages
 * containing data in the form of a {@link android.os.Bundle}
 * </p>
 *
 * <p>
 * This class represents a wrapper around the 
 * {@link android.support.v4.content.LocalBroadcastManager LocalBroadcastManager}.
 * It encapsulates the specific BroadcastReceiver registration, deregistration, 
 * sending and receiving processing needed to fire local broadcast Intents for 
 * particular Intent Actions and receive their contents. It offers the simplified interface
 * {@link com.example.blogpostsplayground.app.LocalMessenger.OnReceiveBroadcastListener 
 * OnReceiveBroadcastListener} which a listener to a particular broadcast intent action has to 
 * implement in order to receive the Bundle with extras send with the Intent.
 * </p>
 *
 * <p>
 * This class is implemented as a Singleton and does not keep any strong references to 
 * any registered listener in order to prevent possible memory leaks. This wrapper also 
 * cleans up any local dependencies related to a registered listener in case its object is
 * collected from the Garbage Collector.
 * </p>
 */
public class LocalMessenger {

    // Private constructor prevents instantiation from other classes
    private LocalMessenger() {
        this.mListeners = new HashMap<String, WeakReference<OnReceiveBroadcastListener>>();
        this.mRegisteredBroadcastReceivers = new HashMap<String, BroadcastReceiver>();
    }

    /**
     * SingletonHolder is loaded on the first execution of Singleton.getInstance()
     * or the first access to SingletonHolder.INSTANCE, not before.
     */
    private static class SingletonHolder {
        private static final LocalMessenger INSTANCE = new LocalMessenger();
    }

    public static LocalMessenger getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /**
     *  Holds weak references to the broadcast listeners
     */
    private HashMap<String, WeakReference<OnReceiveBroadcastListener>> mListeners;

    /**
     *  Holds references to the registered BroadcastReceivers
     */
    private HashMap<String, BroadcastReceiver> mRegisteredBroadcastReceivers;

    /**
     * Builds a new broadcast receiver which calls the subscriber for a particular action
     * and takes care of unregistering itself and cleaning up the subscribers in case
     * the subscriber object reference is not available anymore.
     */
    private BroadcastReceiver getNewBroadcastReceiver() {
        return new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // Check if this key is in the subscribers at all
                if(!mListeners.containsKey(intent.getAction())) {
                    unregisterBroadcastReceiver(context, intent.getAction());
                    return;
                }

                OnReceiveBroadcastListener listener = mListeners.get(intent.getAction()).get();
                if(listener != null) {
                    listener.onReceiveBroadcast(intent.getExtras());
                } else {
                    // Unregister broadcast and remove subscriber weak reference
                    // from the subscribers if the reference is lost
                    unregisterBroadcastReceiver(context, intent.getAction());
                    mListeners.remove(intent.getAction());
                }
            }
        };
    }

    /**
     *  Unregisters BroadcastReceiver for a particular action
     *
     * @param context
     *      Need context to use LocalBroadcastManager
     * @param action
     *      The Broadcast Intent Action
     */
    private void unregisterBroadcastReceiver(Context context, String action) {
        BroadcastReceiver br = mRegisteredBroadcastReceivers.get(action);
        if(br != null) {
            LocalBroadcastManager.getInstance(context).unregisterReceiver(br);
            mRegisteredBroadcastReceivers.remove(action);
        }
    }

    /**
     *  A listener Interface which receivers have to implement in order
     *  to be invoked when broadcast is sent
     */
    public interface OnReceiveBroadcastListener {
        public void onReceiveBroadcast(Bundle extras);
    }

    /**
     *  Sets a listener object for a particular Broadcast Intent Action
     *
     * @param context
     *      Need context to use LocalBroadcastManager
     * @param action
     *      The Broadcast Intent Action
     * @param listener
     *      The listener to be invoked
     */
    public void setListener(Context context, String action, OnReceiveBroadcastListener listener) {
        mListeners.put(action, new WeakReference<OnReceiveBroadcastListener>(listener));

        if(!mRegisteredBroadcastReceivers.containsKey(action)) {
            BroadcastReceiver br = getNewBroadcastReceiver();
            mRegisteredBroadcastReceivers.put(action, br);

            IntentFilter filter = new IntentFilter(action);
            LocalBroadcastManager.getInstance(context).registerReceiver(br, filter);
        }
    }

    /**
     *  Removes subscriber for a particular action
     *
     * @param context
     *      Need context to use LocalBroadcastManager
     * @param action
     *      The Broadcast Intent Action
     */
    public void removeListener(Context context, String action) {
        mListeners.remove(action);
        unregisterBroadcastReceiver(context,action);
    }

    /**
     *  Sends a local broadcast for a particular Intent Action. If there is any
     *  registered receiver for this Action, this method blocks until the receiver
     *  is executed.
     *
     * @param context
     *      Need context to use LocalBroadcastManager
     * @param action
     *      The Broadcast Intent Action
     * @param extras
     *      A bundle with extras that should be sent
     */
    public void sendBlockingBroadcast(Context context, String action, Bundle extras) {
        if(mRegisteredBroadcastReceivers.containsKey(action)) {
            Intent i = new Intent(action);
            i.putExtras(extras);
            LocalBroadcastManager.getInstance(context).sendBroadcastSync(i);
        }
    }

    /**
     *  Sends a local broadcast for a particular Intent Action.
     *
     * @param context
     *      Need context to use LocalBroadcastManager
     * @param action
     *      The Broadcast Intent Action
     * @param extras
     *      A bundle with extras that should be sent
     */
    public void sendBroadcast(Context context, String action, Bundle extras) {
        if(mRegisteredBroadcastReceivers.containsKey(action)) {
            Intent i = new Intent(action);
            i.putExtras(extras);
            LocalBroadcastManager.getInstance(context).sendBroadcast(i);
        }
    }
}

Usage: registering and unregistering

LocalMessenger.getInstance().setListener(getApplicationContext(), 
    "com.example.blogpostsplayground.app.ACTION_AWSUM_MESSAGE", this);
    
LocalMessenger.getInstance().removeListener(getApplicationContext(), 
    "com.example.blogpostsplayground.app.ACTION_AWSUM_MESSAGE");

Usage: sending broadcasts

Bundle extras = new Bundle();
extras.putString("message", "Me block it!");
LocalMessenger.getInstance().sendBlockingBroadcast(getApplicationContext(),
    "com.example.blogpostsplayground.app.ACTION_AWSUM_MESSAGE", extras);

extras.putString("message", "Me like it!");
LocalMessenger.getInstance().sendBroadcast(getApplicationContext(),
    "com.example.blogpostsplayground.app.ACTION_AWSUM_MESSAGE", extras);

Usage: a sample receiver

LocalMessenger.OnReceiveBroadcastListener mListener = 
    new LocalMessenger.OnReceiveBroadcastListener() {
        @Override
        public void onReceiveBroadcast(Bundle extras) {
            Toast.makeText(MainActivity.this, "Awsum broadcast message: " 
                + extras.getString("message"), Toast.LENGTH_SHORT).show();
    }
};