Using Custom Events in Android Apps

There are two major concepts that provide a foundation for building scalable, native Android applications: loosely coupling requests and responses via events, and moving CPU intensive operations off the main thread. This post looks at events. It seems the online documentation on putting together all the pieces of  using custom events is sparse, at best. I’ll show you one example of how to put all the pieces together to create, dispatch and listen for custom events.  The end result is you will be able to decouple your application and make them more flexible and much less likely to break.

I’ve talked about event-based architectures before, and I think they are even more important when building applications for devices. They give you the ability to take into account processor delays and inconsistent internet connections. Between the three pieces of working with custom events described below you should be able to get up and running pretty quickly. I don’t go into much detail on what’s inside the various example as you can find those individual details by searching for them. It’s putting it all together that’s typically the hardest part.

The Event Class. Here’s an example of the content of an Event Class. I’ve taken this several steps further than most examples to include showing how to handle multiple Event types, which are accessed in this example as enums, and implementing multiple listeners. You can also extend your listeners with additional information of just about any type. In this example, I show using String’s for passing messages, but you could just as easily use custom Objects. This should give you a more realistic example of what goes into a commercial application.

	public class MapViewControllerEvent extends EventObject{

		private static final long serialVersionUID = 1L;

		public MapViewControllerEvent(Object source){
			super(source);
		}
	}

	public enum MapViewEvent{
		/**
		 * Connection to the internet has been lost.
		 */
		CONNECTION_LOST("Connection to the internet has been lost."),
		/**
		 * Connection to the internet has been restored.
		 */
		CONNECTION_RESTORED("Connection to the internet has been restored"),
		/**
		 * The LocationService has shutdown. Check logcat for errors.
		 */
		LOCATION_EXCEPTION("There was an unknown error related to the LocationService"),
		/**
		 * Indicates the LocationService is initialized and ready.
		 */
		LOCATION_INITIALIZED("The LocationService has been initialized. NOTE: it still may fail after a start() attempt."),

		private String description;
		private MapViewEvent(String description){
			this.description = description;
		}
	}

	public interface MapViewControllerEventListener extends EventListener{
		/**
		 * Indicates there has been a location change received from LocationService.
		 * @param event
		 * @param message
		 */
		public void onLocationChangeEvent(MapViewControllerEvent event,String message);
		/**
		 * Indicates whether or not device has internet connectivity.
		 * @param event
		 * @param message
		 */
		public void onConnectionChangeEvent(MapViewControllerEvent event,String message);
	}

	protected EventListenerList eventListenerList = new EventListenerList();

	/**
	 * Adds the eventListenerList for MapViewController
	 * @param listener
	 */
	public void addEventListener(MapViewControllerEventListener listener){
		eventListenerList.add(MapViewControllerEventListener.class, listener);
	}

	/**
	 * Removes the eventListenerList for MapViewController
	 * @param listener
	 */
	public void removeEventListener(MapViewControllerEventListener listener){
		eventListenerList.remove(MapViewControllerEventListener.class, listener);
	}

	/**
	 * Dispatches CONNECTION and LOCATION events
	 * @param event
	 * @param message
	 */
	public void dispatchEvent(MapViewControllerEvent event,String message){
		Object[] listeners = eventListenerList.getListenerList();
		Object eventObj = event.getSource();
		String eventName = eventObj.toString();
		for(int i=0; i<listeners.length;i+=2){
			if(listeners[i] == MapViewControllerEventListener.class){
				if(eventName.contains("CONNECTION"))
				{
					((MapViewControllerEventListener) listeners[i+1]).onConnectionChangeEvent(event, message);
				}
				if(eventName.contains("LOCATION")){
					((MapViewControllerEventListener) listeners[i+1]).onLocationChangeEvent(event, message);
				}
			}
		}
	}

Setup Listeners. Here’s how to set up the listener in your Activity. As an example, I called my Event Class MapViewController, but you can name it anything you like:

_mapViewController = new MapViewController(this);
_mapViewController.addEventListener(new MapViewControllerEventListener() {

	@Override
	public void onLocationChangeEvent(MapViewControllerEvent event,
			String message) {
		String eventName = event.getSource().toString();
		if(eventName.contains("EXCEPTION")){
			//TODO let user know
		}
		else{
			//TODO push UX change
		}

	}

	@Override
	public void onConnectionChangeEvent(MapViewControllerEvent event,
			String message) {
		// TODO Auto-generated method stub

	}
});

Dispatch Event. And, here’s how to dispatch an Event:

dispatchEvent(new MapViewControllerEvent(MapViewEvent.LOCATION_INITIALIZED),"Attempting to start location service.");

EventListenerList Class. For some reason, which I would characterize as a major oversite, Android does not currently provide a public EventListenerList Class. This Class makes creating custom events so much easier. However, the folks of the Firefly Client project for Android saved the day and were kind enough to create and post one publicly under an Apache License. The code is a bit dated, and shows some minor warnings when building Android v4, but it will work just fine. You’ll need to include a copy of this Class in your project to make things work.

So, that’s pretty much it. I hope this info helps you build better and more succesful projects.

References:

Java Tutorial – General Information about Writing Event Listeners

Firefly Client Android – EventListenerList Source Code

[Edited: June 7, 2012 – fixed various minor typos]

Best Practices for ActionScript Event Listeners

I see a lot of incorrect instantiation of event listeners, and I’ve also done it wrong quite a few times myself, especially when coding really fast. Doing it right is easy and the following four steps could save you a ton of debugging time later. 

Step 1: First, create the object. In this case I’m creating a Timer object using the ActionScript flash.utils.Timer Class:  

var myTimer:Timer = new Timer(1000);

Step 2: Second, always add your event listeners before executing any of the objects methods. And always, always attach the event listener to an object. Simply calling an addEventListener without attaching it to an object means the listener will receive any events broadcast for that type of event. This can become a nightmare in projects that may have dozens or even hundreds of event listeners running and you have to debug a problem. 

//myTimer is the object to which we will attach the listener.
//timer is the type of listener we want to observe.
//And, timerHandler the function to call when the event occurs.
myTimer.addEventListener(“timer”,timerHandler);

Step 3: Third, now you can call the objects method. I’m going to call the start() method which will kick off the Timer: 

myTimer.start();

Step 4: Lastly, when you are done with an object, always remove the event listener. Leaving event listeners active when they aren’t needed can cause memory leaks. If an object is not removed, it is still registered and it will remain in memory. By removing the event listener, you’ll allow the garbage collector to eventually deallocate it from memory. 

    myTimer.removeEventListener(“timer”,timerHandler);

Why do things in this order? If you add your event listeners after calling a method, such as start(), you can potentially miss an event that occurs right before the event listener is created. By only calling methods after event listeners are created you eliminate the possibility of missing something and banging your head against the wall while trying to debug it under a heavy deadline. 

Here’s a complete Flex 4 sample app that you can play with to try it out: 

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="https://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    xmlns:mx="library://ns.adobe.com/flex/mx">
 <fx:Script>
     <![CDATA[   

       private var myTimer:Timer;

       public function timerHandler(event:TimerEvent):void
       {
          var date:Date = new Date();
          textArea1.text = date.hours.toString()+
             ":"+date.minutes.toString()+
             ":"+date.seconds.toString()+
             ":"+date.milliseconds.toString();
       }   

       protected function start_clickHandler(event:MouseEvent):void
       {
           myTimer = new Timer(1000);
           myTimer.addEventListener("timer", timerHandler);
           myTimer.start();
       }

       protected function stop_clickHandler(event:MouseEvent):void
       {
           myTimer.stop(); 
           myTimer.removeEventListener("timer", timerHandler);
       }
  ]]>
 </fx:Script>

 <s:TextArea id="textArea1" x="15" y="70" width="270" height="20"/>
 <s:Button x="15" y="28" label="Start" click="start_clickHandler(event)"/>
 <s:Button x="103" y="28" label="Stop" click="stop_clickHandler(event)"/>
</s:Application>