Android Delegates
|
Modified: |
Download
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();
}
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++;
}
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;
@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++;
}
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;
@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++;
}
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:
- requires a user class to implement Listener methods
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;
@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) {}
}
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. What is the result of seconds in the following?
seconds=0 seconds++ mov ax, seconds mov ax, 0 inc ax mov seconds, ax mov seconds, ax
- 2. How can we make the counter stop?
- 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 count() {
new Thread(new Runnable() {
public void run() {
while(true)
try {
Thread.sleep(1000);
}
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&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 ParserListenerfor 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. What does extends Activity implements ParserListener mean?
- 2. What does new ITunesSAX(url, this) do?
- 3. What is the purpose of:
- interface ParserListener {
public void setTitle(final String s);
}
- 4. Which thread is executed?
- 5. How many times is the delegate method, setTitle(), executed?
- 6. Are there any race-conditions? If so, where?
- 7. Does the worker thread stop? If so, where and when?
- 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; |
<?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&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&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> : |