This is the last post in this blog series about the Play Services Task API and its use in Firebase. If you missed the first three parts, consider jumping back to those before continuing here. When you're all caught up, let's finish up this series!
Throughout this series, we've only ever talked about units of work that are themselves represented by a Task or a Continuation. In reality, however, there are lots of other ways to get work done. Various utilities and libraries may have their own ways of performing threaded work. You might wonder if you have to switch to the Task API to unify all this if you want to switch to Firebase. But you certainly don't have to. The Task API was designed with the capability of integrating with other ways of doing threaded work.
For example, Java has always had the ability to simply fire up a new thread to process something in parallel with other threads. You can write code like this (though I heartily recommend against it on Android):
new Thread(new Runnable() { @Override public void run() { String result = "the output of some long-running compute"; // now figure out what to do with the result... } }).start();
Here we fire up that new thread from the main thread and do exciting work that ends with a String of interest. All that work that went into creating that string happens in parallel with the main thread, which continued executing after the thread was started. If that threaded work happened to block at any point, the main thread would not be held up by it. However, something must be done to get that String result into the place where it's expected. On Android, if that needs to be back on the main thread, you'll have to write more code to arrange for that to happen. This can get hairy. And we can use Tasks to help.
The Play Services Task API provides a way to make other units of work behave like Tasks, even if they weren't implemented as such. The class of interest here is TaskCompletionSource. This allows you to effectively create a Task "placeholder" that some other bit of code can trigger for success or failure. If you wanted that thread from above to behave like a Task without implementing it as a Task (as we learned last time by passing a Callable to Tasks.call()), you could do this:
final TaskCompletionSource<String> source = new TaskCompletionSource<>(); new Thread(new Runnable() { @Override public void run() { String result = "the output of some long-running compute"; source.setResult(result); } }).start(); Task<String> task = source.getTask(); task.addOnCompleteListener(new OnCompleteListener<String>() { ... });
We now have the thread offering its result String to the TaskCompletionSource using its setResult() method. Then, in the original thread, we simply ask the TaskCompletionSource for its "placeholder" Task, and add a listener to that. The result is now handled inside the listener running on the main thread. You can do the same in the failure case by calling the setException() method on the TaskCompletionSource. That will end up triggering any failure listeners, and they'll get a hold of the exception.
This strategy might seem a little bit silly up front, because there are less verbose ways of putting the result of some work back on the main thread. The value here is in the ability to work with that new placeholder Task along with other Tasks you might be working with in a unified fashion.
Imagine you're writing an app that absolutely depends on some values in Firebase Realtime Database, along with the values in Firebase Remote Config. However, to keep your users entertained while they wait for this data to load, you’d like to create a splash screen that shows some animation until that data is available to work with. Oh, and you don't want that screen to appear and disappear in a jarring way in the event that the data happens to be locally cached, so you want the screen to show for a minimum of 2 seconds. How might you implement this screen?
For starters, you'll need to create a new Activity and design and implement the views for the splash screen. That's straightforward. Then you'll need to coordinate the work between Realtime Database and Remote Config, as well as factor in the two second timer. You'll probably want to kick off all that work during the Activity's onCreate() after you create the splash screen views. You could use a series of Continuations to make sure all these things happen in serial, one after another. But why do that if you could instead start them all at once to run in parallel, and make the user wait only as long as it takes to complete the longest item of work? Let's see how!
The Task API provides a couple methods to help you know when several Tasks are all complete. These static utility methods create a new Task that gets triggered in response to the completion of a collection of Tasks that you provide.
Task<Void> Tasks.whenAll(Collection<? extends Task<?>> tasks) Task<Void> Tasks.whenAll(Task...<?> tasks)
One version of whenAll() accepts a Java Collection (such as a List or Set), and the other uses the varargs style of passing multiple parameters to easily form an array of any length. Either way, the returned Task will now get triggered for success when all the other Tasks succeed, and trigger for failure if any one of them fails. Note that the new Task result is parameterized with Void, meaning it doesn't contain any results directly. If you want the results of each individual Task, you'll have to get the results from each of them directly.
This whenAll() function looks pretty handy for knowing when all our concurrent work is done, so we can move the user past the splash screen. The trick for this case is to somehow get a bunch of Task objects the represent each thing we're waiting on.
The Remote Config fetch is easy, because it will give you a Task you can use to listen to the availability your values. Let's kick off that task and remember it:
private Task<Void> fetchTask; // during onCreate: fetchTask = FirebaseRemoteConfig.getInstance().fetch();
Realtime Database isn't as easy, because it doesn't provide a Task for triggering on the completion of available data. However, we can use the TaskCompletionSource we just learned about to trigger a placeholder task when the data is available:
private TaskCompletionSource<DataSnapshot> dbSource = new TaskCompletionSource<>(); private Task dbTask = dbSource.getTask(); // during onCreate: DatabaseReference ref = FirebaseDatabase.getInstance().getReference("/data/of/interest"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { dbSource.setResult(dataSnapshot); } @Override public void onCancelled(DatabaseError databaseError) { dbSource.setException(databaseError.toException()); } });
Here, we're registering a listener for the data we need to continue launching the app. That listener will then trigger dbTask to success or failure via dbSource depending on the callback it received.
Lastly, there's the minimum two second delay for the splash screen to stay up. We can also represent that delay as a Task using TaskCompletionSource:
private TaskCompletionSource<Void> delaySource = new TaskCompletionSource<>(); private Task<Void> delayTask = delaySource.getTask(); // during onCreate: new Handler().postDelayed(new Runnable() { @Override public void run() { delaySource.setResult(null); } }, 2000);
For the delay, we're just scheduling a Runnable to execute on the main thread after 2000ms, and that Runnable will then trigger delayTask via delaySource.
Now, we have three Tasks, all operating in parallel, and we can use Tasks.whenAll() to create another Task that triggers when they're all successful:
private Task<Void> allTask; // during onCreate(): allTask = Tasks.whenAll(fetchTask, dbTask, delayTask); allTask.addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { DataSnapshot data = dbTask.getResult(); // do something with db data? startActivity(new Intent(SplashScreenActivity.this, MainActivity.class)); } }); allTask.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // apologize profusely to the user! } });
And that should do it! When the final allTask succeeds, we can do whatever we need with the data from the database, then we send the user to MainActivity. Without the use of Tasks here, this code becomes more tedious to write because you'd have to check the state of all the other ongoing units of work at the end of each of them, and proceed only when you know they are all done. Here, the Task API handles those details for you. And you can easily add more Tasks as needed without having to change the logic. Just keep adding Tasks to the collection behind allTask.
It's worth noting that there is a way to block the current thread on the result of one or more Tasks. Normally you don't want to block threads at all, if you can help it, but occasionally it's useful when you have to (such as with Loaders). If you do need to wait on the result of a Task, you can use the await() function:
static <TResult> TResult await(Task<TResult> task) static <TResult> TResult await(Task<TResult> task, long timeout, TimeUnit unit)
With await(), the calling thread simply blocks until the task completes, or the given timeout expires. If it was successful, you'll receive the result object, and if it fails, it will throw an ExecutionException which wraps the underlying cause. Please remember that you should never block the main thread! Only use this when you know you're running on some background thread, OK?
Here's what we covered in the four parts of this blog series:
This should be everything you need to know to make effective use of Play Services Task API! I hope you’re able to use Firebase along with the Task API to make efficient and delightful Android apps.