Front-End Web & Mobile
Using a ContentProvider in Android
This is the third part in a six-part series on synchronizing data within an Android mobile app to the AWS Cloud. Check out the full series:
- An Introduction to the Sync Framework for Android
- Building a ContentProvider for Android
- Using a ContentProvider in Android Mobile Apps (this article)
- Integrating Amazon Cognito with the Android AccountManager API
- Building a Synchronization Endpoint with AWS Mobile Hub
- Building a Synchronization Framework for Android
In our previous post, we wrote about the role of the ContentProvider. The content provider is a custom data management object. This object provides access to your app and other parts of the Android system, such as widgets on the home screen and (probably most importantly) the sync framework that is built into the Android operating system. We also walked through how to implement a SQLite database implementation for a sample notes app. Today, we show you how to use that implementation.
Retrieving a single item
Each Activity within an Android application has a content resolver that can be returned using getContentResolver(). The content resolver determines the correct content provider based on the URI of the request and then routes the request to the appropriate content provider.
Each item within the content provider has a unique URI. For the notes content provider, the URI is based on the authority, table name, and note ID. Each URI has the form:
If you know the ID of the note you want to retrieve, you can obtain it by using the following:
NotesConverter is a static class that contains routines to convert to and from the Note client-side model:
The main thing you need to do when receiving data from the content provider is to convert from data stored in a Cursor to the Note. When writing to the content provider, you need to convert from the Note to the ContentValues object.
Retrieving multiple items
The ContentProvider.query() method takes two parameters that are used to construct the SQLite query – selection and selectionArgs. The selection argument is a string that looks like the WHERE clause of a standard SQLite SELECT statement. However, you can use the question mark to designate arguments. These arguments are listed (in order) in the selectionArgs string array. For example, if you call:
The eventual SQLite call is something like:
You can then page through the data using the standard Cursor methods available in the android.database package.
Using the Loaders Framework
You want to bulk load data to a RecyclerView or ListView object. The best way to do this is to use the built-in Loaders Framework to load data into the activity. The Loaders Framework loads the data asynchronously and then uses callbacks when the loading is complete. Loaders run on separate threads to prevent an unresponsive UI and they cache the results to prevent duplicate queries. In addition, they can automatically register an observer to trigger a reload only when the data changes. The Loaders Framework implements a standard CursorLoader that has this type of observer, which is ideal for use with content providers. Note that we implemented a notifier when writing the content provider in the last article. This notifier works with the observer in the CursorLoader.
To implement a Loader:
- Implement the LoaderManager.LoaderCallbacks interface.
- Initialize the loader in the activity onCreate() method.
To add the LoaderManager interface, adjust the definition of the class:
To initialize the loader, add the following to the end of the onCreate() method:
When the loader is initialized, a new asynchronous task is added to the application. This immediately calls onCreateLoader() to create the loader. This method creates a CursorLoader. The CursorLoader calls the equivalent query() method in the content provider with the same arguments. When the data is available, onLoaderFinished() is called on the UI thread with the new cursor. In this case, you use this to replace the cursor in a RecyclerView.Adapter interface, which then refreshes the data within the list.
Inserting, updating, and deleting items
You should always call mutations asynchronously so that they do not cause unresponsive UI. Fortunately, this is a common requirement, and the content provider interface has already done the majority of the implementation. It’s considered good practice to abstract the call into its own method. For example, we use three mutations within the Notes tutorial – insert, update, and delete:
The AsyncQueryHandler provides the appropriate callbacks for each method. There is an equivalent handler.startNNN method. This method takes an async task ID (that you provide), a cookie (which is passed along to the callback when the async task is complete), and then the same arguments as the call to the content provider. When you call the startNNN method, it creates an async task, calls the content provider, and then calls the callback with the results.
If your underlying database is SQLite, then you do not need to use AsyncQueryHandler because the SQLite database already uses async callbacks for you. However, you should always use AsyncQueryHandler as a best practice in case you want to change the underlying implementation from SQLite to something else (such as Couchbase Lite or an online data store).
Wrap up
In this series so far, we’ve provided an overview of the MyNotes sample code, which is available in the GitHub repository. We’ve also shown how to create a content provider for your application. In the next post, we start to implement a sync adapter by adding an Amazon Cognito authentication source to the built-in Android Account Manager.