Android Delegates
or
Listener Interfaces

Modified

Download

Counter1

Counter2

Counter3

Counter4

Counter5

iTunes Top 10

Android apk

Overview

iOS, as seen in our Top10Parser example, can use delegates for call-backs from one thread to another, providing a consistent, simple pattern.

In particular, we want long-running executions on a separate thread from the UI but updating the UI when needed.

Android does not provide quite the same mechanism but can accomplish many of the benefits of delegates using the Listener pattern.

The key difference is that iOS provides a general means for a thread to call a method on another thread, Android only allows a thread to cal a method that is run on the UI thread.

But that solves one of the biggest problems, having a worker thread pass data to the UI thread.

We first implement 5 (yes 5) counters to examine different approaches and evaluate the value of each.

Finally, we re-implement the iTunes Top 10 using a worker thread and a Listener pattern to delegate UI updates.

Example 1 - Counter with a Handler and handleMessage method

Handlers can over-ride a special method handleMessage which executes on the thread where the handler is defined.

As explained in the Android documentation "When you create a new Handler, it is bound to the thread / message queue of the thread that is creating it -- from that point on, it will deliver messages and runnables to that message queue and execute them as they come out of the message queue."

The handler below is defined in the Activity so executes on that thread that controls the UI.

final Handler countHandler = new Handler() {

      public void handleMessage( Message msg ) {
                count.setText((String) msg.obj);
      }
};

The call-back is made from a worker thread by:

   Message msg = Message.obtain();
   msg.obj = seconds+"";
   countHandler.sendMessage(msg);

Message.obtain() obtains and re-uses an existing message. It defines an Object obj field for referencing a object parameter.

One interesting point is the effect of clicking the Reset button.

What do you think will happen?

package edu.ius.rwisman.Counter1;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class AndroidCounter1Activity extends Activity {	
    TextView count;
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.main);
           count = (TextView) findViewById(R.id.count);
	count.setText("0");
	count();
    }
final Handler countHandler = new Handler() {

      public void handleMessage(Message msg) {
                count.setText((String) msg.obj);
      }
};
     public void reset(View v) {
	count();		
     }
	
     private void count() {
	  new Thread(new Runnable() {
		  int seconds = 0;
		    public void run() {
		    	while(true)
		    	try {
				Thread.sleep(1000);
				seconds++;
   Message msg = Message.obtain();
   msg.obj = seconds+"";
   countHandler.sendMessage(msg);
			}
			catch(Exception e) {
				Log.w("counter Thread Exception ", e+"");
			}
		    }
		  }).start();
	}
}

Example 2 - Counter with a Handler and User Defined Method

One problem with the special method handleMessage is it provides only way for call-back.

Ideally, and more the flavor of delegates, the user defines specific delegate methods that are called by the worker thread.

Below is an attempt to create a user defined method within the handler in the hope that it will also execute on the thread of the creator, in this case the UI thread.

    class CountHandler extends Handler {
       public void setCount(String s) {
                 count.setText(s);
       }
    }

The call-back is made from a worker thread by:

 countHandler.setCount(seconds+"");

What do you think will happen?

package edu.ius.rwisman.Counter2;
 
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class AndroidCounter2Activity extends Activity {
	TextView count;
	CountHandler countHandler;
    class CountHandler extends Handler {
       public void setCount(String s) {
                 count.setText(s);
       }
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.main);
	count = (TextView) findViewById(R.id.count);
	count.setText("0");
	countHandler = new CountHandler();
	count();
    }
    
    public void reset(View v) {
	count();		
    }
	
    private void count() {
	  new Thread(new Runnable() {
	 	 int seconds = 0;
		 public void run() {
		    while(true)
		    	try {
				Thread.sleep(1000);
				seconds++;
   countHandler.setCount(seconds+"");
			}
			catch(Exception e) {
				Log.w("counter Thread Exception ", e+"");
			}
		}
	  }).start();
   }	
}

Example 3 - Counter with a User Defined Method that Works

While the previous example did perform call-backs, they were executed on the worker thread not the UI.

The UI thread must execute the methods of UI objects.

runOnUiThread method executes a Runnable on the UI thread, providing a means for writing a delegate-like call-back from a worker thread that executes on the UI thread.

Below is a user defined method that executes on the UI thread.

public void setCount(final String s) {

     runOnUiThread( new Runnable() {
         public void run() {
               count.setText( s );            
         }
     });
}

The call-back is made from a worker thread by:

setCount(seconds+"");
package edu.ius.rwisman.Counter3;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class AndroidCounter3Activity extends Activity {
      TextView count;
public void setCount(final String s) {

     runOnUiThread( new Runnable() {
         public void run() {
               count.setText( s );            
         }
     });
}
   @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        count = (TextView) findViewById(R.id.count);
        count.setText("0");
        count();
    }
    
    public void reset(View v) {
	count();		
    }
	
    private void count() {
	new Thread(new Runnable() {
		int seconds = 0;
		public void run() {
		    while(true)
		    	try {
				Thread.sleep(1000);
				seconds++;
   setCount(seconds+"");
			}
			catch(Exception e) {
				Log.w("counter Thread Exception ", e+"");
			}
	           }
	}).start();
   }	
}

Example 4 - Counter with a Listener Delegate

The counter works but must be defined in the same class as the call-back.

For modularity and loose-coupling, the two should be separate.

Listener is a common pattern that:

  1. requires a user class to implement Listener methods
     
  2. requires a user class to register as a Listener

The interface to the CounterListerner requires implementing the setCount() method:

interface CounterListener {
     public void setCount(final String s);
}

To be a CounterListener, the class defines itself as an implementer:

public class AndroidCounter4Activity extends Activity implements CounterListener

and defines the setCount() method.

public void setCount(final String s) {

     runOnUiThread( new Runnable() {
         public void run() {
               count.setText( s );            
         }
     });
}

The CounterListener class must also register itself (similar to a delegate) with the Counter class for call-backs.

new Counter( this ).count();

The call-back is made from a worker thread in the Counter class by:

delegate.setCount(seconds+"");

Question - What does the following line do?

 new Counter( this ).count();

package edu.ius.rwisman.Counter4;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class AndroidCounter4Activity extends Activity implements CounterListener {
      TextView count;
public void setCount(final String s) {

     runOnUiThread( new Runnable() {
         public void run() {
               count.setText( s );            
         }
     });
}
   @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        count = (TextView) findViewById(R.id.count);
        count.setText("0");

        new Counter( this ).count();
    }
    
    public void reset(View v) {}
}
interface CounterListener {
    public void setCount(final String s);
}
class Counter {
	CounterListener delegate;
	
	public Counter(CounterListener delegate) {
		this.delegate=delegate;
	}
	
	public void count() {
	  new Thread(new Runnable() {
		  int seconds = 0;
		  public void run() {
		    while(true)
		    	try {
				Thread.sleep(1000);
				seconds++;				// Callback
				delegate.setCount(seconds+"");
			}
			catch(Exception e) {
				Log.w("counter Thread Exception ", e+"");
			}
		  }
	   }).start();
           }	
}

Example 5 - Counter with a Reset

You might have noticed that the reset button created a thread each time clicked.

Correcting that behavior to actually reset the count to 0 is simple and provides some useful insights on threads.

One requirement is a Counter method that sets seconds = 0:

public void reset() {
    synchronized(delegate) {
       seconds = 0;
    }
}

which is called when the Reset button is clicked on the UI thread:

 public void reset(View v) {
     counter.reset();
 }

while seconds is incremented by the worker thread prior to the call-back.

synchronized(delegate) {
     seconds++;
     delegate.setCount(seconds+"");
}

seconds is changed in two places by two different threads creating the potential for a race-condition.

The result of seconds is indeterminate.

Because the two Java operations are not atomic, each consists of several machine instructions. In a multithreaded execution, instruction execution can vary depending upon the thread executed.

Here are two possible outcomes:

seconds=0 seconds++
mov ax, 0  
  mov ax, seconds
mov seconds, ax  
  inc ax
  mov seconds, ax
seconds=0 seconds++
  mov ax, seconds
  inc ax
  mov seconds, ax
mov ax, 0  
mov seconds, ax  

Suppose seconds is 5, what is the result of seconds in each case?

synchronize is used to restrict access to the object delegate to a single thread, all other threads are blocked.

So seconds may be:

incremented and the call-back made then set to 0 or

set to 0 then incremented and the call-back made.

Note that delegate is an object, seconds is not; though usually synchronize access on the object in use, any common object will do.

Questions

  1. 1. What is the result of seconds in the following?
     
    1. seconds=0 seconds++
        mov ax, seconds
      mov ax, 0  
        inc ax
      mov seconds, ax  
        mov seconds, ax

       

  2. 2. How can we make the counter stop?
     
  3. 3. Make necessary changes for a CounterListener method onReset() that is called when seconds is reset to 0.
package edu.ius.rwisman.Counter5;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class AndroidCounter5Activity extends Activity implements CounterListener {
   TextView count;
   Counter counter;
	
   public void setCount(final String s) {
	runOnUiThread(new Runnable() {
		public void run() {
			count.setText(s);			            	
		}
	});
   }
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.main);
	count = (TextView) findViewById(R.id.count);
	count.setText("0");

	counter = new Counter(this);
	counter.count();
    }
    
    public void reset(View v) {
	counter.reset();		
    }
}

interface CounterListener {
	public void setCount(final String s);
}

class Counter {
	CounterListener delegate;
	int seconds = 0;
	
	public Counter(CounterListener delegate) {
		this.delegate=delegate;
	}
public void reset() {
    synchronized(delegate) {
       seconds = 0;
    }
}
	public void count() {
	  new Thread(new Runnable() {
	    public void run() {
	    	while(true)
		    	try {
				Thread.sleep(1000);
   synchronized(delegate) {
      seconds++;
      delegate.setCount(seconds+"");
   }
			}
			catch(Exception e) {
				Log.w("counter Thread Exception ", e+"");
			}
	    }
	  }).start();
	}	
}

 

Commonly, rather than:

public class AndroidCounter5Activity extends Activity implements CounterListener

and

counter = new Counter(this);
counter.count();

listeners are implemented using an inner-class definition as below:

	CounterListener cl = new CounterListener() {
	        public void setCount(final String s) {
	    		runOnUiThread(new Runnable() {
	    			public void run() {
	    				count.setText(s);		            	
	    			}
	    		});
	        }
	        public void onReset() {
	    		runOnUiThread(new Runnable() {
	    			public void run() {
	    				count.setText("Reset");			            	
	    			}
	    		});
	        }
	}; 
		
	counter = new Counter( cl );
	counter.count();

 

Example - iTunes Top 10

iTunes publishes the ranked listing of downloaded tunes as RSS, an XML data structure commonly used for news feeds, weather, blogs, etc.

The application parses the RSS to locate the <title> element. The relevant parts of the RSS feed are listed below.

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
        xmlns:content="http://purl.org/rss/1.0/modules/content/"
        xmlns:itms="http://phobos.apple.com/rss/1.0/modules/itms/">
  <channel>
    <title>iTunes Top 10 Songs</title>
    <link>http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewTop?id=1&amp;popId=1</link>
    <description>iTunes Store: Today's Top 10 Songs</description>
    <image>
        <url>/images/rss/badge.gif</url>
        <link>http://www.apple.com/itunes/</link>
        <title>iTunes Music Store</title>
        <height>31</height><width>88</width>
    </image>
    <item>
       <title>1. My Life Would Suck Without You - Kelly Clarkson</title>
       <title>2. Gives You Hell - The All-American Rejects</title>
                                :

The iTunes URL to return the top 10 songs is:

Key Points

The logic is much simpler, fewer parameters to pass, no getter methods, no arrays of anything. The data is delivered to the UI thread as it becomes available.

The Activity implements ParserListener

public class AndroidITunesListenerActivity extends Activity
                  implements ParserListener

for the ParserListener call-back method

public void setTitle(final String s) {
    runOnUiThread(new Runnable() {
        public void run() {
           TextView title = new TextView(activity);
            title.setText(s);
            layout.addView(title);
        }
    });
}

and registers itself with the ITunesSAX object:

iTunesSAX = new ITunesSAX(url, this);

 ITunesSAX class

1. receives the iTunes URL and ParserListener class

2. downloads and parses the XML

3. appends the text between <title> </title> to a string

4. at each </title>, executes call-back with the string of all that title's text.

Questions

  1. 1. What does extends Activity implements ParserListener mean?
     
  2. 2. What does new ITunesSAX(url, this) do?
     
  3. 3. What is the purpose of:
     
    1. interface ParserListener {
      public void setTitle(final String s);
      }
       
  4. 4. Which thread is executed?
     
  5. 5. How many times is the delegate method, setTitle(), executed?
     
  6. 6. Are there any race-conditions? If so, where?
     
  7. 7. Does the worker thread stop? If so, where and when?
  8. 8. Modify to perform a call-back to ParserListener method parseComplete() when parsing is completed.
     
package edu.ius.rwisman.AndroidITunesListener;

import java.io.*;
import android.app.Activity;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.net.URL;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;

public class AndroidITunesListenerActivity extends Activity implements ParserListener{
	
	LinearLayout layout;
	AndroidITunesListenerActivity activity = this;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		layout = new LinearLayout(this);
		layout.setOrientation(1);
		setContentView(layout);
		
		try {
			URL url = new URL(
			"http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wpa/MRSS/topsongs/limit=10/rss.xml");

			new ITunesSAX(url, this);
		}
		catch(Exception e) { System.out.println("Exception " + e);  }
	}

	public void setTitle(final String s) {
		runOnUiThread(new Runnable() {
			public void run() {
	    		   TextView title = new TextView(activity);
	    		   title.setText(s);
	    		   layout.addView(title);
			}
		});
	}
  }

interface ParserListener {
	public void setTitle(final String s);
}

class ITunesSAX extends DefaultHandler {
    Boolean itemFound=false;
    ParserListener delegate;
    String element="";
 
    public ITunesSAX(final URL url, ParserListener delegate) {
     this.delegate = delegate;

     final ITunesSAX iTunesSAX = this;
     
     new Thread(new Runnable() {
    	  public void run() {
    	       SAXParserFactory factory = SAXParserFactory.newInstance();    // Default (non-validating) parser

   	        try {
   	        	InputStream in = url.openStream();
  	            SAXParser saxParser = factory.newSAXParser();             // Parse the input
   	            saxParser.parse( in, iTunesSAX);
  	        } catch (Throwable t) {  t.printStackTrace(); }
   	 }
     }).start();
   }

    public void startElement(String namespaceURI, String sName,      // simple name
                                           String qName,                                       // qualified name
                                           Attributes attrs) throws SAXException { 

	if( sName.equals("item") ) itemFound = true;
	element = "";
    }

    public void endElement(String namespaceURI, String sName,        // simple name
                       	        String qName                                            // qualified name
                        	                   ) throws SAXException {
	if(itemFound && sName.equals("title") )
		delegate.setTitle(element);		                   // Call-back to user 
    }

    public void characters(char[] buf, int offset, int length)  throws SAXException {
       if( length <= 0) return;
       element = element + new String(buf, offset, length);
    }
    public void startDocument() throws SAXException {}
    public void endDocument() throws SAXException {}
}

 

For reference, below is the XML parsing in a worker thread, using a handler for call-backs.

package edu.ius.rwisman.iTunesSAX;

import java.io.*;
import android.app.Activity;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.os.Handler;

import java.net.URL;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;

public class ITunesSAXActivity extends Activity {
	
    LinearLayout layout;

    TextView titleView[] = new TextView[10];

    ITunesSAXActivity activity = this;
    ITunesSAX iTunesSAX;
	
    final Handler handler = new Handler();   

    final Runnable updateUI = new Runnable() {
       public void run() {

    	   String titleString[] = iTunesSAX.getTitle();

    	   for(int i=0; i<10;i++) {
    		   titleView[i] = new TextView(activity);

    		   titleView[i].setText( titleString[i] );

    		   layout.addView( titleView[i] );
    	   }
       }
    };   

    @Override
    public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	
	layout = new LinearLayout(this);						// Create a new layout to display the view
	layout.setOrientation(1);
	
	setContentView(layout);							// Set the layout view to display
		
	try {
	   URL url = new URL(
		 "http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wpa/MRSS/topsongs/limit=10/rss.xml");

	   iTunesSAX = new ITunesSAX(url, handler, updateUI);
	}
	catch(Exception e) { System.out.println("Exception " + e);  }
     }
}

class ITunesSAX extends DefaultHandler {
    Boolean itemFound=false;
    final Runnable updateUI;
    final Handler handler;
    String titleString[]=new String[10];
    String element="";
    int n=0;

    public ITunesSAX(final URL url, final Handler handler, final Runnable updateUI) {
     this.handler = handler;
     this.updateUI = updateUI;

     final ITunesSAX iTunesSAX = this;						// this is an ITunesSAX object
     
     new Thread(new Runnable() {

         public void run() {
    	SAXParserFactory factory = SAXParserFactory.newInstance();

   	try {
   	        	  InputStream in = url.openStream();
  	            SAXParser saxParser = factory.newSAXParser();

   	            saxParser.parse( in, iTunesSAX);             			// Parse the input

    	            handler.post( updateUI ); 					// Parse complete
  	} catch (Throwable t) {
   	            t.printStackTrace();
   	}
        }
     }).start();
   }
  
    public void startDocument() throws SAXException {}
    public void endDocument() throws SAXException {}

    public void startElement(String namespaceURI, String sName,      	// simple name
                      					    String qName, 	// qualified name
                         Attributes attrs) throws SAXException { 

	if( sName.equals("item") ) itemFound = true;

	element = "";
    }

    public void endElement(String namespaceURI, String sName,       	// simple name
                       					   String qName       	// qualified name
                        			) throws SAXException {

	if(itemFound && sName.equals("title") ) {
    	   	titleString[n] = element;
    	   	n++;
       	}
     }

     public void characters(char[] buf, int offset, int length)  throws SAXException {
       	if( length <= 0) return;

       	element = element + new String(buf, offset, length);
     }
}
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
        xmlns:content="http://purl.org/rss/1.0/modules/content/"
        xmlns:itms="http://phobos.apple.com/rss/1.0/modules/itms/">
  <channel>
    <title>iTunes Top 10 Songs</title>
    <link>http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewTop?id=1&amp;popId=1</link>
    <description>iTunes Store: Today's Top 10 Songs</description>
    <image>
        <url>/images/rss/badge.gif</url>
        <link>http://www.apple.com/itunes/</link>
        <title>iTunes Music Store</title>
        <height>31</height><width>88</width>
    </image>
    <item>
       <title>1. My Life Would Suck Without You - Kelly Clarkson</title>
                                :
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
        xmlns:content="http://purl.org/rss/1.0/modules/content/"
        xmlns:itms="http://phobos.apple.com/rss/1.0/modules/itms/">
  <channel>
    <title>iTunes Top 10 Songs</title>
    <link>http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewTop?id=1&amp;popId=1</link>
    <description>iTunes Store: Today's Top 10 Songs</description>
    <image>
        <url>/images/rss/badge.gif</url>
        <link>http://www.apple.com/itunes/</link>
        <title>iTunes Music Store</title>
        <height>31</height><width>88</width>
    </image>
    <item>
       <title>1. My Life Would Suck Without You - Kelly Clarkson</title>
       <title>2. Gives You Hell - The All-American Rejects</title>
                                :