In our previous posts we discussed SQLite access and Content Providers. Now access to a content provider will involve accessing some sort of persistent storage like a database or a file under the hood. Accessing a storage can be a long task and hence this should not be executed on the UI thread which will delay the rendering and could even lead to ANRs. Hence, background threads should be responsible for handling provider’s execution.
Spawning new threads in the ContentProvider
itself is not a good idea especially in the case of query()
as you’ll then have to block and wait for the background thread to return the result (cursor) making it not asynchronous. Ideally execution must occur in a background thread and then the result communicated back to the UI thread. Now this could be implemented with Runnables, Threads and Handlers but Android provides us with a special class called AsyncQueryHandler
just for this purpose. This abstract class exposes an excellent interface to deal with the situation.
What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.
Note: Ideally the background threads should be created by the application component that uses the provider by creating a ContentResolver
object. If the caller and provider are in the same application process, then the provider methods are invoked on the same thread as the ContentResolver
methods. However, if the processes are different then the provider implementation is invoked on binder threads (processes incoming IPCs).
Using AsyncQueryHandler
AsyncQueryHandler
is basically an abstract class that wraps the ContentResolver
object and handles background execution of its operations (CRUD) as well as passing messages (result) from the between threads (background and main/UI). It has four methods that wraps that of a ContentResolver
:
-
startQuery(int token, Object cookie, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy)
-
startInsert(int token, Object cookie, Uri uri, ContentValues initialValues)
-
startUpdate(int token, Object cookie, Uri uri, ContentValues values, String selection, String[] selectionArgs)
-
startDelete(int token, Object cookie, Uri uri, String selection, String[] selectionArgs)
If you notice carefully, except the first two parameters, all others match their equivalent ContentResolver
methods. Each of them, when called, execute the equivalent ContentResolver
method on a background thread. When the provider is done with its operation, it sends the result back to AsyncQueryHandler
that invokes the following callbacks that your implementation should override.
class MyQueryHandler extends AsyncQueryHandler { public MyQueryHandler(ContentResolver cr) { super(cr); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { // query() completed } @Override protected void onInsertComplete(int token, Object cookie, Uri uri) { // insert() completed } @Override protected void onUpdateComplete(int token, Object cookie, int result) { // update() completed } @Override protected void onDeleteComplete(int token, Object cookie, int result) { // delete() completed } }
The last parameter for all the callbacks are results which has the same type as that of the return value of the underlying ContentResolver
methods (query()
, insert()
, update()
, delete()
). Let’s discuss the first two methods of the calls and callbacks:
- Token: It’s a user-defined request code that identifies what kind of a request this is. This can be used with
cancelOperation()
to cancel unprocessed requests submitted with that token. - Cookie: It’s a data container object that can be passed from the request to the response (callback) so that it can be used for some purpose like identifying the request if necessary.
AsyncQueryHandler
is mostly created and executes provider operations on the UI thread, but it can be done on any other thread. The callbacks are always called on the thread that creates the AsyncQueryHandler
.
Let’s get in action with some code now. We’ll convert the query()
code from our content provider article to make use of AsyncQueryHandler
:
// MainActivity.onCreate() // AsyncQueryHandler object AsyncQueryHandler queryHandler = new AsyncQueryHandler(getContentResolver()) { @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { int idIndex = cursor.getColumnIndex(UserDictionary.Words._ID); int wordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD); int localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); if (cursor == null) { // Some providers return null if an error occurs whereas others throw an exception } else if (cursor.getCount() < 1) { // No matches found } else { while (cursor.moveToNext()) { int id = cursor.getInt(idIndex); String word = cursor.getString(wordIndex); String locale = cursor.getString(localeIndex); // Dumps "ID: 1 Word: NewWord Locale: en_US" // I added this via Settings > Language & Input > Personal Dictionary Log.d(TAG, "ID: " + id + " Word: " + word + " Locale: " + locale); } } } }; // Construct query and execute // "projection" defines the columns that will be returned for each row String[] projection = { UserDictionary.Words._ID, // Contract class constant for the _ID column UserDictionary.Words.WORD, // Contract class constant for the word column UserDictionary.Words.LOCALE // Contract class constant for the locale column }; // Defines WHERE clause columns and placeholders String selectionClause = UserDictionary.Words._ID + " = ?"; // Define the WHERE clause's placeholder values String[] selectionArgs = { "1" }; queryHandler.startQuery( 1, null, UserDictionary.Words.CONTENT_URI, projection, selectionClause, selectionArgs, UserDictionary.Words.DEFAULT_SORT_ORDER // "frequency DESC" );
It’s all pretty straightforward. We create a AsyncQueryHandler
object that implements the onQueryComplete()
callback. On the object we execute startQuery()
which is similar to ContentResolver.query()
except the first two arguments which are token (1
) and cookie (null
). The entire query operation happens in the background and finally when the result set is ready, onQueryComplete()
is called in the UI thread where you can use the cursor
however you want to (display results or just log).
Similarly you can also insert, update or delete records using relevant methods and callbacks.
Under the Hood
It’ll actually help us to understand how things work behidn the scenes with regards to AsyncQueryHandler
. When one of these methods is called – startQuery()
, startInsert()
, startUpdate()
, startDelete()
– a Message
object is created that is populated with the ContentResolver
arguments, cookie
object data and the token
which becomes the what
field of the Message
(can be later used to cancelOperation()
). This Message
object is added to the MessageQueue
of a single background thread. All AsynQueryHandler
instances within an application adds these provider requests messages to this same queue on a single background thread. This is not really problematic as accessing providers (eventually data sources) are not very long tasks as a network operation.
Once the background thread processes a request, the result is passed back in a Message
object to the calling AsyncQueryHandler
instance. Then the data is extracted and passed to the respective callbacks. This also means all the provider requests are handled sequentially.
Wrapping Up
In this article we discussed how AsyncQueryHandler
can be used to so conveniently to execute our provider’s operations in a background thread without blocking the UI thread. In the next article we’ll dive into another concept called CursorLoader
that can be used for similar purpose.