In Understanding Concurrency on Android Using HaMeR, we talked about the basics of the HaMeR (Handler
, Message
, and Runnable
) framework. We covered its options, as well as when and how to use it.
Today, we’ll create a simple application to explore the concepts learned. With a hands-on approach, we’ll see how to apply the different possibilities of HaMeR in managing concurrency on Android.
1. The Sample Application
Let’s get to work and post some Runnable
and send Message
objects on a sample application. To keep it as simple as possible, we’ll explore only the most interesting parts. All resource files and standard activity calls will be ignored here. So I strongly advise you to check out the source code of the sample application with its extensive comments.
The app will consist of:
- Two activities, one for
Runnable
another forMessage
calls - Two
HandlerThread
objects:-
WorkerThread
to receive and process calls from the UI -
CounterThread
to receiveMessage
calls from theWorkerThread
-
- Some utility classes (to preserve objects during configuration changes and for layout)
2. Posting and Receiving Runnables
Let's begin experimenting with the Handler.post(Runnable)
method and its variations, which add a runnable to a MessageQueue
associated with a thread. We'll create an activity called RunnableActivity
, which communicates with a background thread called WorkerThread
.
The RunnableActivity
instantiates a background thread called WorkerThread
, passing a Handler
and a WorkerThread.Callback
as parameters. The activity can make calls on WorkerThread
to asynchronously download a bitmap and exhibit a toast at a certain time. The results of the tasks done by the worker thread are passed to RunnableActivity
by runnables posted on the Handler
received by WorkerThread
.
2.1 Preparing a Handler for RunnableActivity
On the RunnableActivity
we'll create a Handler
to be passed to WorkerThread
. The uiHandler
will be associated with the Looper
from the UI thread, since it's being called from that thread.
public class RunnableActivity extends Activity { // Handler that allows communication between // the WorkerThread and the Activity protected Handler uiHandler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // prepare the UI Handler to send to WorkerThread uiHandler = new Handler(); } }
2.2 Declaring WorkerThread
and Its Callback Interface
The WorkerThread
is a background thread where we'll start different kinds of tasks. It communicates with the user interface using the responseHandler
and a callback interface received during its instantiation. The references received from the activities are WeakReference<>
type, since an activity could be destroyed and the reference lost.
The class offers an interface that can be implemented by the UI. It also extends HandlerThread
, a helper class built on top of Thread
that already contains a Looper
, and a MessageQueue
. Hence it has the correct setup to use the HaMeR framework.
public class WorkerThread extends HandlerThread { /** * Interface to facilitate calls on the UI. */ public interface Callback { void loadImage(Bitmap image); void showToast(String msg); } // This Handler will be responsible only // for posting Runnables on this Thread private Handler postHandler; // Handler is received from the MessageActivity and RunnableActivity // responsible for receiving Runnable calls that will be processed // on the UI. The callback will help this process. private WeakReference<Handler> responseHandler; // Callback from the UI // it is a WeakReference because it can be invalidated // during "configuration changes" and other events private WeakReference<Callback> callback; private final String imageAUrl = "https://pixabay.com/static/uploads/photo/2016/08/05/18/28/mobile-phone-1572901_960_720.jpg"; /** * The constructor receives a Handler and a Callback from the UI * @param responseHandler in charge of posting the Runnable to the UI * @param callback works together with the responseHandler * allowing calls directly on the UI */ public WorkerThread(Handler responseHandler, Callback callback) { super(TAG); this.responseHandler = new WeakReference<>(responseHandler); this.callback = new WeakReference<>(callback); } }
2.3 Initializing WorkerThread
We need to add a method to WorkerThread
to be called by the activities that prepare the thread's postHandler
for use. The method needs to be called only after the thread is started.
public class WorkerThread extends HandlerThread { /** * Prepare the postHandler. * It must be called after the thread has started */ public void prepareHandler() { postHandler = new Handler(getLooper()); } }
On the RunnableActivity
we must implement WorkerThread.Callback
and initialize the thread so it can be used.
public class RunnableActivity extends Activity implements WorkerThread.Callback { // BackgroundThread responsible for downloading the image protected WorkerThread workerThread; /** * Initialize the {@link WorkerThread} instance * only if it hasn't been initialized yet. */ public void initWorkerThread(){ if ( workerThread == null ) { workerThread = new WorkerThread(uiHandler, this); workerThread.start(); workerThread.prepareHandler(); } } /** * set the image downloaded on bg thread to the imageView */ @Override public void loadImage(Bitmap image) { myImage.setImageBitmap(image); } @Override public void showToast(final String msg) { // to be implemented } }
2.4 Using Handler.post()
on the WorkerThread
The WorkerThread.downloadWithRunnable()
method downloads a bitmap and sends it to RunnableActivity
to be displayed in an image View. It illustrates two basic uses of the Handler.post(Runnable run)
command:
-
To allow a Thread to post a Runnable object to a MessageQueue associated with itself when
.post()
is called on a Handler associated with the Thread's Looper. - To allow communication with other Threads, when
.post()
is called on a Handler associated with other Thread's Looper.
- The
WorkerThread.downloadWithRunnable()
method posts aRunnable
to theWorkerThread
'sMessageQueue
using thepostHandler
, aHandler
associated withWorkThread
'sLooper
. - When the runnable is processed, it downloads a
Bitmap
on theWorkerThread
. - After the bitmap is downloaded, the
responseHandler
, a handler associated with the UI thread, is used to post a runnable on theRunnableActivity
containing the bitmap. - The runnable is processed, and the
WorkerThread.Callback.loadImage
is used to exhibit the downloaded image on anImageView
.
public class WorkerThread extends HandlerThread { /** * post a Runnable to the WorkerThread * Download a bitmap and sends the image * to the UI {@link RunnableActivity} * using the {@link #responseHandler} with * help from the {@link #callback} */ public void downloadWithRunnable() { // post Runnable to WorkerThread postHandler.post(new Runnable() { @Override public void run() { try { // sleeps for 2 seconds to emulate long running operation TimeUnit.SECONDS.sleep(2); // Download image and sends to UI downloadImage(imageAUrl); } catch (InterruptedException e) { e.printStackTrace(); } } }); } /** * Download a bitmap using its url and * send to the UI the image downloaded */ private void downloadImage(String urlStr){ // Create a connection HttpURLConnection connection = null; try { URL url = new URL(urlStr); connection = (HttpURLConnection) url.openConnection(); // get the stream from the url InputStream in = new BufferedInputStream(connection.getInputStream()); final Bitmap bitmap = BitmapFactory.decodeStream(in); if ( bitmap != null ) { // send the bitmap downloaded and a feedback to the UI loadImageOnUI( bitmap ); } else { } } catch (IOException e) { e.printStackTrace(); } finally { if ( connection != null ) connection.disconnect(); } } /** * sends a Bitmap to the ui * posting a Runnable to the {@link #responseHandler} * and using the {@link Callback} */ private void loadImageOnUI(final Bitmap image){ Log.d(TAG, "loadImageOnUI("+image+")"); if (checkResponse() ) { responseHandler.get().post( new Runnable() { @Override public void run() { callback.get().loadImage(image); } } ); } } // verify if responseHandler is available // if not the Activity is passing by some destruction event private boolean checkResponse(){ return responseHandler.get() != null; } }
2.5 Using Handler.postAtTime()
and Activity.runOnUiThread()
The WorkerThread.toastAtTime()
schedules a task to be executed at a certain time, exhibiting a Toast
to the user. The method illustrates the use of the Handler.postAtTime()
and the Activity.runOnUiThread()
.
-
Handler.postAtTime(Runnable run, long uptimeMillis)
posts a runnable at a given time.
-
Activity.runOnUiThread(Runnable run)
uses the default UI handler to post a runnable to the main thread.
public class WorkerThread extends HandlerThread { /** * show a Toast on the UI. * schedules the task considering the current time. * It could be scheduled at any time, we're * using 5 seconds to facilitates the debugging */ public void toastAtTime(){ Log.d(TAG, "toastAtTime(): current - " + Calendar.getInstance().toString()); // seconds to add on current time int delaySeconds = 5; // testing using a real date Calendar scheduledDate = Calendar.getInstance(); // setting a future date considering the delay in seconds define // we're using this approach just to facilitate the testing. // it could be done using a user defined date also scheduledDate.set( scheduledDate.get(Calendar.YEAR), scheduledDate.get(Calendar.MONTH), scheduledDate.get(Calendar.DAY_OF_MONTH), scheduledDate.get(Calendar.HOUR_OF_DAY), scheduledDate.get(Calendar.MINUTE), scheduledDate.get(Calendar.SECOND) + delaySeconds ); Log.d(TAG, "toastAtTime(): scheduling at - " + scheduledDate.toString()); long scheduled = calculateUptimeMillis(scheduledDate); // posting Runnable at specific time postHandler.postAtTime( new Runnable() { @Override public void run() { if ( callback != null ) { callback.get().showToast( "Toast called using 'postAtTime()'." ); } } }, scheduled); } /** * Calculates the {@link SystemClock#uptimeMillis()} to * a given Calendar date. */ private long calculateUptimeMillis(Calendar calendar){ long time = calendar.getTimeInMillis(); long currentTime = Calendar.getInstance().getTimeInMillis(); long diff = time - currentTime; return SystemClock.uptimeMillis() + diff; } }
public class RunnableActivity extends Activity implements WorkerThread.Callback { /** * Callback from {@link WorkerThread} * Uses {@link #runOnUiThread(Runnable)} to illustrate * such method */ @Override public void showToast(final String msg) { Log.d(TAG, "showToast("+msg+")"); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show(); } }); } }
3. Sending Messages With the MessageActivity
& WorkerThread
Next, let's explore some different ways of using MessageActivity
to send and process Message
objects. The MessageActivity
instantiates WorkerThread
, passing a Handler
as a parameter. The WorkerThread
has some public methods with tasks to be called by the activity to download a bitmap, download a random bitmap, or exhibit a Toast
after some delayed time. The results of all those operations are sent back to MessageActivity
using Message
objects sent by the responseHandler
.
3.1 Preparing the Response Handler From MessageActivity
As in the RunnableActivity
, in the MessageActivity
we'll have to instantiate and initialize a WorkerThread
sending a Handler
to receive data from the background thread. However, this time we won't implement WorkerThread.Callback
; instead, we'll receive responses from the WorkerThread
exclusively by Message
objects.
Since most of the MessageActivity
and RunnableActivity
code is basically the same, we'll concentrate only on the uiHandler
preparation, which will be sent to WorkerThread
to receive messages from it.
First, let's provide some int
keys to be used as identifiers to the Message objects.
public class MessageActivity extends Activity { // Message identifier used on Message.what() field public static final int KEY_MSG_IMAGE = 2; public static final int KEY_MSG_PROGRESS = 3; public static final int KEY_MSG_TOAST = 4; }
On MessageHandler
implementation, we'll have to extend Handler
and implement the handleMessage(Message)
method, where all messages will be processed. Notice that we're fetching Message.what
to identify the message, and we're also getting different kinds of data from Message.obj
. Let's quickly review the most important Message
properties before diving into the code.
-
Message.what
:int
identifying theMessage
-
Message.arg1
:int
arbitrary argument -
Message.arg2
:int
arbitrary argument -
Message.obj
:Object
to store different kinds of data
public class MessageActivity extends Activity { /** * Handler responsible to manage communication * from the {@link WorkerThread}. It sends Messages * back to the {@link MessageActivity} and handle * those Messages */ public class MessageHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { // handle image case KEY_MSG_IMAGE:{ Bitmap bmp = (Bitmap) msg.obj; myImage.setImageBitmap(bmp); break; } // handle progressBar calls case KEY_MSG_PROGRESS: { if ( (boolean) msg.obj ) progressBar.setVisibility(View.VISIBLE); else progressBar.setVisibility(View.GONE); break; } // handle toast sent with a Message delay case KEY_MSG_TOAST:{ String msgText = (String)msg.obj; Toast.makeText(getApplicationContext(), msgText, Toast.LENGTH_LONG ).show(); break; } } } } // Handler that allows communication between // the WorkerThread and the Activity protected MessageHandler uiHandler; }
3.2 Sending Messages With WorkerThread
Now let's get back to the WorkerThread
class. We'll add some code to download a specific bitmap and also code to download a random one. To accomplish those tasks, we'll send Message
objects from the WorkerThread
to itself and send the results back to MessageActivity
using exactly the same logic applied earlier for the RunnableActivity
.
First we need to extend the Handler
to process the downloaded messages.
public class WorkerThread extends HandlerThread { // send and processes download Messages on the WorkerThread private HandlerMsgImgDownloader handlerMsgImgDownloader; /** * Keys to identify the keys of {@link Message#what} * from Messages sent by the {@link #handlerMsgImgDownloader} */ private final int MSG_DOWNLOAD_IMG = 0; // msg that download a single img private final int MSG_DOWNLOAD_RANDOM_IMG = 1; // msg that download random img /** * Handler responsible for managing the image download * It send and handle Messages identifying then using * the {@link Message#what} * {@link #MSG_DOWNLOAD_IMG} : single image * {@link #MSG_DOWNLOAD_RANDOM_IMG} : random image */ private class HandlerMsgImgDownloader extends Handler { private HandlerMsgImgDownloader(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { showProgressMSG(true); switch ( msg.what ) { case MSG_DOWNLOAD_IMG: { // receives a single url and downloads it String url = (String) msg.obj; downloadImageMSG(url); break; } case MSG_DOWNLOAD_RANDOM_IMG: { // receives a String[] with multiple urls // downloads a image randomly String[] urls = (String[]) msg.obj; Random random = new Random(); String url = urls[random.nextInt(urls.length)]; downloadImageMSG(url); } } showProgressMSG(false); } } }
The downloadImageMSG(String url)
method is basically the same as the downloadImage(String url)
method. The only difference is that the first sends the downloaded bitmap back to the UI by sending a message using the responseHandler
.
public class WorkerThread extends HandlerThread { /** * Download a bitmap using its url and * display it to the UI. * The only difference with {@link #downloadImage(String)} * is that it sends the image back to the UI * using a Message */ private void downloadImageMSG(String urlStr){ // Create a connection HttpURLConnection connection = null; try { URL url = new URL(urlStr); connection = (HttpURLConnection) url.openConnection(); // get the stream from the url InputStream in = new BufferedInputStream(connection.getInputStream()); final Bitmap bitmap = BitmapFactory.decodeStream(in); if ( bitmap != null ) { // send the bitmap downloaded and a feedback to the UI loadImageOnUIMSG( bitmap ); } } catch (IOException e) { e.printStackTrace(); } finally { if ( connection != null ) connection.disconnect(); } } }
The loadImageOnUIMSG(Bitmap image)
is responsible for sending a message with the downloaded bitmap to MessageActivity
.
/** * sends a Bitmap to the ui * sending a Message to the {@link #responseHandler} */ private void loadImageOnUIMSG(final Bitmap image){ if (checkResponse() ) { sendMsgToUI( responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_IMAGE, image) ); } } /** * Show/Hide progressBar on the UI. * It uses the {@link #responseHandler} to * send a Message on the UI */ private void showProgressMSG(boolean show){ Log.d(TAG, "showProgressMSG()"); if ( checkResponse() ) { sendMsgToUI( responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_PROGRESS, show) ); } }
Notice that instead of creating a Message
object from scratch, we're using the Handler.obtainMessage(int what, Object obj)
method to retrieve a Message
from the global pool, saving some resources. It's also important to note that we're calling the obtainMessage()
on the responseHandler
, obtaining a Message
associated with MessageActivity
's Looper
. There are two ways to retrieve a Message
from the global pool: Message.obtain()
and Handler.obtainMessage()
.
The only thing left to do on the image download task is to provide the methods to send a Message
to WorkerThread
to start the download process. Notice that this time we'll call Message.obtain(Handler handler, int what, Object obj)
on handlerMsgImgDownloader
, associating the message with WorkerThread
's looper.
/** * sends a Message to the current Thread * using the {@link #handlerMsgImgDownloader} * to download a single image. */ public void downloadWithMessage(){ Log.d(TAG, "downloadWithMessage()"); showOperationOnUIMSG("Sending Message..."); if ( handlerMsgImgDownloader == null ) handlerMsgImgDownloader = new HandlerMsgImgDownloader(getLooper()); Message message = Message.obtain(handlerMsgImgDownloader, MSG_DOWNLOAD_IMG,imageBUrl); handlerMsgImgDownloader.sendMessage(message); } /** * sends a Message to the current Thread * using the {@link #handlerMsgImgDownloader} * to download a random image. */ public void downloadRandomWithMessage(){ Log.d(TAG, "downloadRandomWithMessage()"); showOperationOnUIMSG("Sending Message..."); if ( handlerMsgImgDownloader == null ) handlerMsgImgDownloader = new HandlerMsgImgDownloader(getLooper()); Message message = Message.obtain(handlerMsgImgDownloader, MSG_DOWNLOAD_RANDOM_IMG, imagesUrls); handlerMsgImgDownloader.sendMessage(message); }
Another interesting possibility is sending Message
objects to be processed at a later time with the command Message.sendMessageDelayed(Message msg, long timeMillis)
.
/** * Show a Toast after a delayed time. * * send a Message with delayed time on the WorkerThread * and sends a new Message to {@link MessageActivity} * with a text after the message is processed */ public void startMessageDelay(){ // message delay long delay = 5000; String msgText = "Hello from WorkerThread!"; // Handler responsible for sending Message to WorkerThread // using Handler.Callback() to avoid the need to extend the Handler class Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { responseHandler.get().sendMessage( responseHandler.get().obtainMessage(MessageActivity.KEY_MSG_TOAST, msg.obj) ); return true; } }); // sending message handler.sendMessageDelayed( handler.obtainMessage(0,msgText), delay ); }
We created a Handler
expressly for sending the delayed message. Instead of extending the Handler
class, we took the route of instantiating a Handler
by using the Handler.Callback
interface, for which we implemented the handleMessage(Message msg)
method to process the delayed Message
.
4. Conclusion
You've seen enough code by now to understand how to apply the basic HaMeR framework concepts to manage concurrency on Android. There are some other interesting features of the final project stored on GitHub, and I strongly advise you to check it out.
Finally, I have some last considerations that you should keep in mind:
-
Don't forget to consider Android's Activity lifecycle when working with HaMeR and Threads in general. Otherwise, your app may fail when the thread tries to access activities that have been destroyed due to configuration changes or for other reasons. A common solution is to use a
RetainedFragment
to store the thread and populate the background thread with the activity's reference every time the activity is destroyed. Take a look at the solution in the final project on GitHub. - Tasks that run due to
Runnable
andMessage
objects processed onHandlers
don't run asynchronously. They'll run synchronously on the thread associated with the handler. To make it asynchronous, you'll need to create another thread, send/post theMessage
/Runnable
object on it, and receive the results at the appropriate time.
As you can see, the HaMeR framework has lots of different possibilities, and it's a fairly open solution with a lot of options for managing concurrency on Android. These characteristics can be advantages over AsyncTask
, depending on your needs. Explore more of the framework and read the documentation, and you'll create great things with it.
See you soon!
Comments