Bolts is a collection of low-level libraries designed to make developing mobile
apps easier. Bolts was designed by Parse and Facebook for our own internal use,
and we have decided to open source these libraries to make them available to
others. Using these libraries does not require using any Parse services. Nor
do they require having a Parse or Facebook developer account.
Bolts includes:
"Tasks", which make organization of complex asynchronous code more manageable. A task is kind of like a JavaScript Promise, but available for iOS and Android.
An implementation of the App Links protocol, helping you link to content in other apps and handle incoming deep-links.
To build a truly responsive Android application, you must keep long-running operations off of the UI thread, and be careful to avoid blocking anything the UI thread might be waiting on. This means you will need to execute various operations in the background. To make this easier, we've added a class called Task. A Task represents an asynchronous operation. Typically, a Task is returned from an asynchronous function and gives the ability to continue processing the result of the task. When a Task is returned from a function, it's already begun doing its job. A Task is not tied to a particular threading model: it represents the work being done, not where it is executing. Tasks have many advantages over other methods of asynchronous programming, such as callbacks and AsyncTask.
They consume fewer system resources, since they don't occupy a thread while waiting on other Tasks.
Performing several Tasks in a row will not create nested "pyramid" code as you would get when using only callbacks.
Tasks are fully composable, allowing you to perform branching, parallelism, and complex error handling, without the spaghetti code of having many named callbacks.
You can arrange task-based code in the order that it executes, rather than having to split your logic across scattered callback functions.
For the examples in this doc, assume there are async versions of some common Parse methods, called saveAsync and findAsync which return a Task. In a later section, we'll show how to define these functions yourself.
The continueWith Method
Every Task has a method named continueWith which takes a Continuation. A continuation is an interface that you implement which has one method, named then. The then method is called when the Task is complete. You can then inspect the Task to check if it was successful and to get its result.
saveAsync(obj).continueWith(newContinuation<ParseObject, Void>() {
publicVoidthen(Task<ParseObject> task) throwsException {
if (task.isCancelled()) {
// the save was cancelled.
} elseif (task.isFaulted()) {
// the save failed.Exceptionerror = task.getError();
} else {
// the object was saved successfully.ParseObjectobject = task.getResult();
}
returnnull;
}
});
Tasks are strongly-typed using Java Generics, so getting the syntax right can be a little tricky at first. Let's look closer at the types involved with an example.
/** Gets a String asynchronously. */publicTask<String> getStringAsync() {
// Let's suppose getIntAsync() returns a Task<Integer>.returngetIntAsync().continueWith(
// This Continuation is a function which takes an Integer as input,// and provides a String as output. It must take an Integer because// that's what was returned from the previous Task.newContinuation<Integer, String>() {
// The Task getIntAsync() returned is passed to "then" for convenience.publicStringthen(Task<Integer> task) throwsException {
Integernumber = task.getResult();
returnString.format("%d", Locale.US, number);
}
}
);
}
In many cases, you only want to do more work if the previous Task was successful, and propagate any errors or cancellations to be dealt with later. To do this, use the onSuccess method instead of continueWith.
saveAsync(obj).onSuccess(newContinuation<ParseObject, Void>() {
publicVoidthen(Task<ParseObject> task) throwsException {
// the object was saved successfully.returnnull;
}
});
Chaining Tasks Together
Tasks are a little bit magical, in that they let you chain them without nesting. If you use continueWithTask instead of continueWith, then you can return a new task. The Task returned by continueWithTask will not be considered complete until the new Task returned from within continueWithTask is. This lets you perform multiple actions without incurring the pyramid code you would get with callbacks. Likewise, onSuccessTask is a version of onSuccess that returns a new task. So, use continueWith/onSuccess to do more synchronous work, or continueWithTask/onSuccessTask to do more asynchronous work.
By carefully choosing whether to call continueWith or onSuccess, you can control how errors are propagated in your application. Using continueWith lets you handle errors by transforming them or dealing with them. You can think of failed Tasks kind of like throwing an exception. In fact, if you throw an exception inside a continuation, the resulting Task will be faulted with that exception.
finalParseQuery<ParseObject> query = ParseQuery.getQuery("Student");
query.orderByDescending("gpa");
findAsync(query).onSuccessTask(newContinuation<List<ParseObject>, Task<ParseObject>>() {
publicTask<ParseObject> then(Task<List<ParseObject>> task) throwsException {
List<ParseObject> students = task.getResult();
students.get(0).put("valedictorian", true);
// Force this callback to fail.thrownewRuntimeException("There was an error.");
}
}).onSuccessTask(newContinuation<ParseObject, Task<List<ParseObject>>>() {
publicTask<List<ParseObject>> then(Task<ParseObject> task) throwsException {
// Now this continuation will be skipped.ParseObjectvaledictorian = task.getResult();
returnfindAsync(query);
}
}).continueWithTask(newContinuation<List<ParseObject>, Task<ParseObject>>() {
publicTask<ParseObject> then(Task<List<ParseObject>> task) throwsException {
if (task.isFaulted()) {
// This error handler WILL be called.// The exception will be "There was an error."// Let's handle the error by returning a new value.// The task will be completed with null as its value.returnnull;
}
// This will also be skipped.List<ParseObject> students = task.getResult();
students.get(1).put("salutatorian", true);
returnsaveAsync(students.get(1));
}
}).onSuccess(newContinuation<ParseObject, Void>() {
publicVoidthen(Task<ParseObject> task) throwsException {
// Everything is done! This gets called.// The task's result is null.returnnull;
}
});
It's often convenient to have a long chain of success callbacks with only one error handler at the end.
Creating Tasks
When you're getting started, you can just use the Tasks returned from methods like findAsync or saveAsync. However, for more advanced scenarios, you may want to make your own Tasks. To do that, you create a TaskCompletionSource. This object will let you create a new Task and control whether it gets marked as completed or cancelled. After you create a Task, you'll need to call setResult, setError, or setCancelled to trigger its continuations.
It's similarly easy to create saveAsync, findAsync or deleteAsync. We've also provided some convenience functions to help you create Tasks from straight blocks of code. callInBackground runs a Task on our background thread pool, while call tries to execute its block immediately.
Task.callInBackground(newCallable<Void>() {
publicVoidcall() {
// Do a bunch of stuff.
}
}).continueWith(...);
Tasks in Series
Tasks are convenient when you want to do a series of asynchronous operations in a row, each one waiting for the previous to finish. For example, imagine you want to delete all of the comments on your blog.
ParseQuery<ParseObject> query = ParseQuery.getQuery("Comments");
query.whereEqualTo("post", 123);
findAsync(query).continueWithTask(newContinuation<List<ParseObject>, Task<Void>>() {
publicTask<Void> then(Task<List<ParseObject>> results) throwsException {
// Create a trivial completed task as a base case.Task<Void> task = Task.forResult(null);
for (finalParseObjectresult : results) {
// For each item, extend the task with a function to delete the item.task = task.continueWithTask(newContinuation<Void, Task<Void>>() {
publicTask<Void> then(Task<Void> ignored) throwsException {
// Return a task that will be marked as completed when the delete is finished.returndeleteAsync(result);
}
});
}
returntask;
}
}).continueWith(newContinuation<Void, Void>() {
publicVoidthen(Task<Void> ignored) throwsException {
// Every comment was deleted.returnnull;
}
});
Tasks in Parallel
You can also perform several Tasks in parallel, using the whenAll method. You can start multiple operations at once and use Task.whenAll to create a new Task that will be marked as completed when all of its input Tasks are finished. The new Task will be successful only if all of the passed-in Tasks succeed. Performing operations in parallel will be faster than doing them serially, but may consume more system resources and bandwidth.
ParseQuery<ParseObject> query = ParseQuery.getQuery("Comments");
query.whereEqualTo("post", 123);
findAsync(query).continueWithTask(newContinuation<List<ParseObject>, Task<Void>>() {
publicTask<Void> then(Task<List<ParseObject>> results) throwsException {
// Collect one task for each delete into an array.ArrayList<Task<Void>> tasks = newArrayList<Task<Void>>();
for (ParseObjectresult : results) {
// Start this delete immediately and add its task to the list.tasks.add(deleteAsync(result));
}
// Return a new task that will be marked as completed when all of the deletes are// finished.returnTask.whenAll(tasks);
}
}).onSuccess(newContinuation<Void, Void>() {
publicVoidthen(Task<Void> ignored) throwsException {
// Every comment was deleted.returnnull;
}
});
Task Executors
All of the continueWith and onSuccess methods can take an instance of java.util.concurrent.Executor as an optional second argument. This allows you to control how the continuation is executed. Task.call() invokes Callables on the current thread and Task.callInBackground will use its own thread pool, but you can provide your own executor to schedule work onto a different thread. For example, if you want to do work on a specific thread pool:
finalRequestrequest = ...
Task.call(newCallable<HttpResponse>() {
@OverridepublicHttpResponsecall() throwsException {
// Work is specified to be done on NETWORK_EXECUTORreturnclient.execute(request);
}
}, NETWORK_EXECUTOR).continueWithTask(newContinuation<HttpResponse, Task<byte[]>>() {
@OverridepublicTask<byte[]> then(Task<HttpResponse> task) throwsException {
// Since no executor is specified, it's continued on NETWORK_EXECUTORreturnprocessResponseAsync(response);
}
}).continueWithTask(newContinuation<byte[], Task<Void>>() {
@OverridepublicTask<Void> then(Task<byte[]> task) throwsException {
// We don't want to clog NETWORK_EXECUTOR with disk I/O, so we specify to use DISK_EXECUTORreturnwriteToDiskAsync(task.getResult());
}
}, DISK_EXECUTOR);
For common cases, such as dispatching on the main thread, we have provided default implementations of Executor. These include Task.UI_THREAD_EXECUTOR and Task.BACKGROUND_EXECUTOR. For example:
One difficulty in breaking up code across multiple callbacks is that they have different variable scopes. Java allows functions to "capture" variables from outer scopes, but only if they are marked as final, making them immutable. This is inconvenient. That's why we've added another convenience class called Capture, which lets you share a mutable variable with your callbacks. Just call get and set on the variable to change its value.
// Capture a variable to be modified in the Task callbacks.finalCapture<Integer> successfulSaveCount = newCapture<Integer>(0);
saveAsync(obj1).onSuccessTask(newContinuation<ParseObject, Task<ParseObject>>() {
publicTask<ParseObject> then(Task<ParseObject> obj1) throwsException {
successfulSaveCount.set(successfulSaveCount.get() + 1);
returnsaveAsync(obj2);
}
}).onSuccessTask(newContinuation<ParseObject, Task<ParseObject>>() {
publicTask<ParseObject> then(Task<ParseObject> obj2) throwsException {
successfulSaveCount.set(successfulSaveCount.get() + 1);
returnsaveAsync(obj3);
}
}).onSuccessTask(newContinuation<ParseObject, Task<ParseObject>>() {
publicTask<ParseObject> then(Task<ParseObject> obj3) throwsException {
successfulSaveCount.set(successfulSaveCount.get() + 1);
returnsaveAsync(obj4);
}
}).onSuccess(newContinuation<ParseObject, Void>() {
publicVoidthen(Task<ParseObject> obj4) throwsException {
successfulSaveCount.set(successfulSaveCount.get() + 1);
returnnull;
}
}).continueWith(newContinuation<Void, Integer>() {
publicIntegerthen(Task<Void> ignored) throwsException {
// successfulSaveCount now contains the number of saves that succeeded.returnsuccessfulSaveCount.get();
}
});