11.1B: Add a content provider to your database

Contents:

Content providers in real apps are more complex than the basic version you built in the previous practical.

In the real world:

  • The backend is a database, file system, or other persistent storage option.
  • The front-end displays the data in a pleasing UI and allows users to manipulate the data.

You will rarely build an app from scratch. More often, you will debug, refactor, or extend an existing application.

In this practical, you will take the WordListSQL app and refactor and extend it to use a content provider as a layer between the SQL database and the RecyclerView.

You can go about this in two ways.

  • Refactor and extend the WordListSQL app. This involves changing the app architecture and refactoring code.
  • Start from scratch and re-use code from WordListSQL and MinimalistContentProvider.

The practical will demonstrate how to refactor the existing WordListSQL app, because it's what you are more likely to encounter on the job.

What you should already KNOW

For this practical you should be familiar with how to:

  • Display data in a RecyclerView.
  • Start and return from an Activity.
  • Create, change, and interact with a SQLite database using a SQLiteOpenHelper.
  • Understand the architecture the minimal content provider you built in the previous practical.

What you will LEARN

You will learn how to:

  • Create a fully developed content provider for an existing application.
  • Refactor an application to accommodate a content provider.

What you will DO

This practical requires setup that is more typical for real-word app development.

You start with the WordListSQLInteractive app you created in a previous practical, which displays words from a SQLite database in a RecyclerView, and users can create, edit, and delete words.

You will extend and modify this app:

  • Implement a Contract class to expose your app's interface to other apps.
  • Implement a ContentProvider and query it using a ContentResolver.
  • Refactor the MainActivity, WordListAdapter, and WordListOpenHelper classes to work with the content provider.

App Overview

The completed WordListSQLWithContentProvider app will have the following features:

  • A content provider that can insert, delete, update, and query the database.
  • A contract and permissions set that allow other apps to access this content provider.
  • A content resolver that interacts with the content provider to insert, delete, update, and query data.
  • Unchanged user interface and functionality.

Your app will look that same as at the end of the data storage practical. Screen of  the final app

App component overview

The following diagram shows an overview of the components of an app that uses a SQLiteDatabase with a content provider. The only difference from the minimal content provider app is that the content provider fetches the data from a database through an open helper.

App components overview.

The diagram below shows the architecture of the WorldListSQLInteractive app with a content provider added; this is the WordListSQLWithContentProvider app that you will build in this practical.

Components of the finished WordListSQLWithContentProvider app.

See the concepts chapter for a detailed explanation of all the components and how they interact.

Changes overview

This is a summary of the changes you will make to WordListInteractive to add a content provider.

  • New Classes: Contract, ContentProvider, ContentResolver
  • Classes that change: WordListOpenHelper, MainActivity, WordListAdapter
  • Classes that should not change: WordItem, MyButtonOnClickListener, ViewHolder

Task 1. Download and run the base code

This practical builds on the WordListSQLInteractive and MinimalistContentProvider apps that you built previously. You will extend a copy of WordListSQLInteractive. You can start from your own code, or download the apps.

Task 2. Add a Contract class to WordListSQLInteractive

You will start by creating a contract class that defines public database constants, URI constants, and the MIME types. You will use these constants in all the other classes. The first piece to implement is the Contract class.

2.1 Add a Contract class

  1. Study the Define a Schema and Contract documentation.
  2. Add a new public final class to your project and call it Contract.

    This Contract class contains all the information that another app needs to use your app's content provider. You could name the class anything, but it is customarily called "Contract".

    public final class Contract {}
    
  3. To prevent the Contract class from being instantiated, add a private, empty constructor.

    This is a standard pattern for classes that are used to hold meta information and constants for an app.

    private Contract() {}
    

2.2 Move database constants into Contract

Move the constants for the database that another app would need to know out of WordListOpenHelper into the contract and make them public.

  1. Move DATABASE_NAME and make it public.

    public static final String DATABASE_NAME = "wordlist";
    

    Create a static abstract inner class for each table with the column names. This inner class commonly implements the BaseColumns interface. By implementing the BaseColumns interface, your class can inherit a primary key field called _ID that some Android classes, such as cursor adapters, expect to exist. These inner classes are not required, but can help your database work well with the Android framework.

  2. Create an inner class WordList that implements BaseColumns.
    public static abstract class WordList implements BaseColumns {
    }
    
  3. Move WORD_LIST_TABLE name, as well as KEY_ID and KEY_WORD column names from WordListOpenHelper into the WordList class in Contract, and make them public.
  4. Go back to WorldListOpenHelper and wait for Android Studio to import the constants from the Contract; or import them manually, if you are not set up for auto-imports.

    Use File > Settings > Editor > General > Auto Import on Windows/Linux or Android Studio > Preferences >Editor >General > Auto Import on Mac to configure automated imports.)

2.3 Define URI Constants

  1. Declare the URI scheme for your content provider.

    Using the Contract in MinimalistContentProvider as an example, declare AUTHORITY, CONTENT_PATH. Add CONTENT_PATH_URI to return all items, and ROW_COUNT_URI that returns the number of entries. In the AUTHORITY, use your app's name.

    public static final int ALL_ITEMS = -2;
    public static final String COUNT = "count";
    
    public static final String AUTHORITY =
           "com.android.example.wordlistsqlwithcontentprovider.provider";
    
    public static final String CONTENT_PATH = "words";
    
    public static final Uri CONTENT_URI =
           Uri.parse("content://" + AUTHORITY + "/" + CONTENT_PATH);
    public static final Uri ROW_COUNT_URI =
           Uri.parse("content://" + AUTHORITY + "/" + CONTENT_PATH + "/" + COUNT);
    

2.4 Declare the MIME types

The MIME type describes the type and format of data. The MIME types is used to process the data appropriately. Common MIME types include text/html for web pages, and application/json. Read more about MIME types for content providers in the Android documentation.

  1. Declare MIME types for single and multiple record responses:
    static final String SINGLE_RECORD_MIME_TYPE =
       "vnd.android.cursor.item/vnd.com.example.provider.words";
    static final String MULTIPLE_RECORDS_MIME_TYPE =
       "vnd.android.cursor.item/vnd.com.example.provider.words";
    
  2. Run your app. It should run and look and act exactly as before you changed it.

Task 3. Create a Content Provider

The second piece to implement is the ContentProvider class.

In this task you will create a content provider, implement its query method, and hook it up with the WordListAdapter and the WordListOpenHelper. Instead of querying the WordListOpen Helper, the WordListAdapter will use a content resolver to query the content provider, which in turn will query WordListOpenHelper which will query the database. Query progression through app components.

3.1 Create a WordListContentProvider class

  1. Create a new class that extends ContentProvider and call it WordListContentProvider.
  2. In Android Studio, click on the red lightbulb, select "Implement methods", and click OK to implement all listed methods.
  3. Specify a log TAG.
  4. Declare a UriMatcher.

    This content provider uses an UriMatcher, a utility class that maps URIs to numbers, so you can switch on them.

    private static UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    
  5. Declare a WordListOpenHelper class variable, mDB.
    private WordListOpenHelper mDB;
    
  6. Declare the codes for the URI matcher as constants.

    This puts the codes in one place and makes them easy to change. Use tens, so that inserting additional codes is straightforward.

    private static final int URI_ALL_ITEMS_CODE = 10;
    private static final int URI_ONE_ITEM_CODE = 20;
    private static final int URI_COUNT_CODE = 30;
    
  7. Change the onCreate() method to
    • initialize mDB with a WordListOpenHelper,
    • call the initializeUriMatching() method that you will create next,
    • and return true.
      @Override
      public boolean onCreate() {
         mDB = new WordListOpenHelper(getContext());
         initializeUriMatching();
         return true;
      }
      
  8. Create a private void method initializeUriMatching().
  9. In initializeUriMatching(), add URIs to the matcher for getting all items, one item, and the count.

    Refer to the Contract and use the initializeUriMatching() method in the MinimalistContentProver app as a template.

Solution:

private void initializeUriMatching(){
   sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH, URI_ALL_ITEMS_CODE);
   sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH + "/#", URI_ONE_ITEM_CODE);
   sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH + "/" + Contract.COUNT, URI_COUNT_CODE );
}

3.2 Implement WordListContentProvider.query()

Use the MiniContentProvider as a template to implement the query() method.

  1. Modify WordListContentProvider.query().
  2. Use a Switch statement for the codes returned by sUriMatcher.
  3. For URI_ALL_ITEMS_CODE, URI_ONE_ITEM_CODE, URI_COUNT_CODE, call the corresponding in WordListOpenHelper (mDB).

Notice how assigning the results from mDB.query() to a cursor, generates an error, because WordListOpenHelper.query() returns a WordItem.

Notice how assigning the results from mDB.count() to a cursor generates an error, because WordListOpenHelper.count() returns a long.

You will fix both these errors next.

Solution:

@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection,
                   String[] selectionArgs, String sortOrder) {

   Cursor cursor = null;

   switch (sUriMatcher.match(uri)) {
       case URI_ALL_ITEMS_CODE:
           cursor = mDB.query(ALL_ITEMS);
           break;

       case URI_ONE_ITEM_CODE:
           cursor = mDB.query(parseInt(uri.getLastPathSegment()));
           break;

       case URI_COUNT_CODE:
           cursor = mDB.count();
           break;

       case UriMatcher.NO_MATCH:
           // You should do some error handling here.
           Log.d(TAG, "NO MATCH FOR THIS URI IN SCHEME: " + uri);
           break;
       default:
           // You should do some error handling here.
           Log.d(TAG, "INVALID URI - URI NOT RECOGNIZED: "  + uri);
   }
   return cursor;
}

3.3 Fix WordListOpenHelper.query() to return a Cursor and handle returning all items

Fix WordListOpenHelper.query() to return cursor.

Since the content provider works with cursors, you can simplify the WordListOpenHelper.query() method to return a cursor.

  1. Add code with a query to return all items from the database to handle the cursor = mDB.query(ALL_ITEMS) case from the above switch statement.
  2. Simplify WordListOpenHelper.query() to return a cursor.

    This fixes the error in WordListContentProvider.query().

    However, this breaks WordListAdapter.OnBindViewHolder(), which expects a WordItem from WordListOpenHelper.

To resolve this, WordListAdapter.onBindViewHolder() needs to use a content resolver instead of calling the database directly, which you will do after fixing WordListContentProvider.count().

Note: This kind of cascading errors and fixes is typical for working with real-life applications. If an app you are working with is well architected, you can fix the errors one by one.

Solution:

/**
 * Queries the database for an entry at a given position.
 *
 * @param position The Nth row in the table.
 * @return a WordItem with the requested database entry.
 */
public Cursor query(int position) {
    String query;
    if (position != ALL_ITEMS) {
        position++; // Because database starts counting at 1.
        query = "SELECT " + KEY_ID + "," + KEY_WORD + " FROM "
                 + WORD_LIST_TABLE
                 +" WHERE " + KEY_ID + "=" + position + ";";
    } else {
        query = "SELECT  * FROM " + WORD_LIST_TABLE
                 + " ORDER BY " + KEY_WORD + " ASC ";
    }

    Cursor cursor = null;
    try {
        if (mReadableDB == null) {
            mReadableDB = this.getReadableDatabase();
        }
        cursor = mReadableDB.rawQuery(query, null);
    } catch (Exception e) {
        Log.d(TAG, "QUERY EXCEPTION! " + e);
    } finally {
        return cursor;
    }
}

3.4 Fix WordListOpenHelper.count() to return a Cursor

Since the content provider works with cursors, you must also change the WordListOpenHelper.count() method to return a cursor.

Use a MatrixCursor, which is a cursor of changeable rows and columns.

  1. Create a MatrixCursor using Contract.CONTENT_PATH.
  2. Inside a try block, get the count and add it as a row to the cursor.
  3. Return the cursor.

Solution:

public Cursor count(){
   MatrixCursor cursor = new MatrixCursor(new String[] {Contract.CONTENT_PATH});
   try {
       if (mReadableDB == null) {
           mReadableDB = getReadableDatabase();
   }
       int count =  (int) DatabaseUtils.queryNumEntries(mReadableDB, WORD_LIST_TABLE);
       cursor.addRow(new Object[]{count});
   } catch (Exception e) {
       Log.d(TAG, "EXCEPTION " + e);
   }
   return cursor;
}

This fixes the error in WordListContentProvider.count(), but breaks WordListAdapter.getItemCount(), which expects a long from WordListOpenHelper.

In WordListAdapter.onBindViewHolder(), instead of calling the database directly, you will have to use content resolvers, which you will do next.

3.5 Fix WordListAdapter.onBindViewHolder() to use a content resolver

Next, you will fix WordListAdapter.onBindViewHolder() to use a content resolver instead of calling the WordListOpenHelper directly. Change onBindViewHolder to use a content resolver

  1. In WordListAdapter, delete the mDB variable, since you are not directly referencing the database anymore. This shows errors in Android Studio that will guide your subsequent changes.
  2. In the constructor, delete the assignment to mDB.
  3. Refactor > Change the signature of the constructor and remove the db parameter.
  4. Add instance variables for the query parameters since they will be used more than once.

    The content resolver takes a query parameter, which you must build. The query is similarly structured to a SQL query, but instead of a selection statement, it uses a URI. Query parameters are very similar to SQL queries.

    private String queryUri = Contract.CONTENT_URI.toString(); // base uri
    private static final String[] projection = new String[] {Contract.CONTENT_PATH}; //table
    private String selectionClause = null;
    private String selectionArgs[] = null;
    private String sortOrder = "ASC";
    
  5. In onBindViewholder(), delete the first two lines of code.
    • WordItem current - mDB.query(position);
    • holder.wordItemView.setText(current.getWord());
  6. Define an empty String variable named word.
  7. Define an integer variable called id and set it to -1.
  8. Create a content resolver with the specified query parameters and store the results in a Cursor called cursor. (See MainActivity of MinimalistContentProvider app for an example.)

    String word = "";
    int id = -1;
    
    Cursor cursor = mContext.getContentResolver().query(Uri.parse(
                        queryUri), null, null, null, sortOrder);
    
  9. Instead of just getting a WordItem delivered, WordListAdapter.onBindViewHolder() has to do the extra work of extracting the word from the cursor returned by the content resolver.
    • If the returned cursor contains data, extract the word and set the text of the view holder.
    • Extract the id, because you'll need it for the click listeners.
    • Close the cursor. Remember that you did not close the cursor in WordListOpenHelper.query(), because you returned it.
    • Handle the case of no data in the cursor.
    • Implement any referenced string resources.
if (cursor != null) {
      if (cursor.moveToPosition(position)) {
            int indexWord = cursor.getColumnIndex(Contract.WordList.KEY_WORD);
            word = cursor.getString(indexWord);
            holder.wordItemView.setText(word);
            int indexId = cursor.getColumnIndex(Contract.WordList.KEY_ID);
            id = cursor.getInt(indexId);
       } else {
            holder.wordItemView.setText(R.string.error_no_word);
       }

       cursor.close();

} else {
       Log.e (TAG, "onBindViewHolder: Cursor is null.");
}
  1. Fix the parameters for the click listeners for the two buttons:

    • current.getId() ⇒ id
    • current.getWord() ⇒ word

    The updated click listener for the DELETE button looks like this:

    @Override
    public void onClick(View v) {
    selectionArgs = new String[]{Integer.toString(id)};
    int deleted = mContext.getContentResolver().delete(
        Contract.CONTENT_URI, Contract.CONTENT_PATH,selectionArgs);
     if (deleted > 0) {
         // Need both calls
         notifyItemRemoved(h.getAdapterPosition());
         notifyItemRangeChanged(
                 h.getAdapterPosition(), getItemCount());
     } else {
          Log.d (TAG, mContext.getString(R.string.not_deleted) + deleted);
     }
    }
    
  2. Replace the call to mDB.delete(id) in the DELETE button callback with a content resolver call to delete.
    selectionArgs = new String[]{Integer.toString(id)};
    int deleted = mContext.getContentResolver().delete(
                   Contract.CONTENT_URI, Contract.CONTENT_PATH, selectionArgs);
    

3.6 Change WordListAdapter.getItemCount() to use a content resolver

Instead of requesting the count from the database, getItemCount() has to connect to the content resolver and request the count. In the Contract, you defined a URI for getting that count:

public static final String COUNT = "count";
public static final Uri ROW_COUNT_URI =
       Uri.parse("content://" + AUTHORITY + "/" + CONTENT_PATH + "/" + COUNT

Change WordListAdaptergetItemCount() to:

  • Use a content resolver query to get the item count
  • Use the ROW_COUNT_URI in your query
  • The count is an integer type and is the first element of the returned Cursor
  • Extract count from the cursor and return it
  • Return -1 otherwise
  • Close the cursor

Use the code you just wrote for onBindViewHolder as a guideline.

Solution:

@Override
public int getItemCount() {
    Cursor cursor = mContext.getContentResolver().query(
                     Contract.ROW_COUNT_URI, new String[] {"count(*) AS count"},
                     selectionClause, selectionArgs, sortOrder);
     try {
         cursor.moveToFirst();
         int count = cursor.getInt(0);
         cursor.close();
         return count;
     } catch (Exception e){
         Log.d(TAG, "EXCEPTION getItemCount: " + e);
         return -1;
     }
  }

3.7 Add the content provider to the Android Manifest

  1. Run your app.
  2. Examine logcat for the (very common) cause of the error.
  3. Add the content provider to the Android Manifest inside the <application> tag.
    <provider
       android:name=".WordListContentProvider"  android:authorities="com.android.example.wordlistsqlwithcontentprovider.provider">
    </provider>
    
  4. Run your app.

Your app should run and be fully functional. If it is not, compare your code to the supplied solution code, and use the debugger and logging to find the problem.

3.8 What's next?

  • You have implemented a content provider and its query() method.
  • You followed the errors to update methods in the WordListOpenHelper and WordListAdapter classes to work with the content provider.
  • When you run your app, for queries, the method calls go through the content provider.
  • For the insert, delete, and update operations, your app is still calling WordListOpenHelper.

With the infrastructure you have built, implementing the remaining methods will be a lot less work.

Task 4. Implement Content Provider methods

4.1 getType()

The getType() method is called by other apps that want to use this content provider, to discover what kind of data your app returns.

Use a switch statement to return the appropriate MIME types.

  • The MIME types are listed in the contract.
  • SINGLE_RECORD_MIME_TYPE is for URI_ALL_ITEMS_CODE
  • MULTIPLE_RECORDS_MIME_TYPE is for URI_ONE_ITEM_CODE

Solution:

@Nullable
@Override
public String getType(Uri uri) {
   switch (sUriMatcher.match(uri)) {
       case URI_ALL_ITEMS_CODE:
           return MULTIPLE_RECORDS_MIME_TYPE;
       case URI_ONE_ITEM_CODE:
           return SINGLE_RECORD_MIME_TYPE;
       default:
           return null;
   }
}

Challenge: How can you test this method, as it is not called by your app. Can you think of three different ways of testing that this method works correctly?

4.2 Call the content provider to insert and update words in MainActivity

To fix insert operations MainActivity().onActivityResult needs to call the content provider instead of the database for inserting and updating words.

  1. In MainActivity, delete the declaration of mDB and its instantiation.

In OnActivityResult()

Inserting:

  1. If the word length is not zero, create a ContentValues variable named "values" and add the user-inputted word to it using the string "word" as a key.
  2. Replace mDB.insert(word); with an insert request to a to a content resolver.

Updating:

  1. Replace mDB.update(id, word); with an update request to a to a content resolver.

Solution snippet:

// Update the database
if (word.length() != 0) {
   ContentValues values = new ContentValues();
   values.put(Contract.WordList.KEY_WORD, word);
   int id = data.getIntExtra(WordListAdapter.EXTRA_ID, -99);

   if (id == WORD_ADD) {
      getContentResolver().insert(Contract.CONTENT_URI, values);
   } else if (id >=0) {
       String[] selectionArgs = {Integer.toString(id)};
       getContentResolver().update(Contract.CONTENT_URI, values, Contract.WordList.KEY_ID, selectionArgs
);
   }
   // Update the UI
   mAdapter.notifyDataSetChanged();

4.3 Implement insert() in the content provider

The insert() method in the content provider is a pass-through. So you

  1. call the OpenHelper insert() method,
  2. convert the returned long id to a content URI to the inserted item,
  3. and return that URI.

Android Studio reports an error for the values parameter, which you will fix in the next steps.

Solution:

public Uri insert(Uri uri, ContentValues values) {
   long id = mDB.insert(values);
   return Uri.parse(CONTENT_URI + "/" + id);
}

4.4 Fix insert() in WordListOpenHelper

Android Studio reports an error for the values parameter.

  1. Open WordListOpenHelper. The insert() method is written to take a String parameter.
  2. Change the parameter to be of type ContentValues.
  3. Delete the declaration and assignment of values in the body of the method.

4.5 Implement update() in the content provider

Fix the update methods in the same way as you fixed the insert methods.

  1. In WordListContentProvider, Implement update(), which is one line of code that passes the id and the word as arguments.
    return mDB.update(parseInt(selectionArgs[0]),
    values.getAsString(Contract.WordList.KEY_WORD));
    
  2. You don't need to make any changes to update in WordListOpenHelper.

4.6 Implement delete() in the content provider

In WordListContentProvider, Implement the delete() method by calling the delete() method in WordListOpenHelper with the id of the word to delete.

return mDB.delete(parseInt(selectionArgs[0]));

4.7 Run your app

Yup. That's it. Run your app and make sure everything works.

And if your app still doesn't work, you should correct any issues. You will need the working code in a later practical. In that lesson you will write an app that uses this content provider to load word list data into its user interface.

Solution code

Android Studio project: word_list_sql_with_content_provider

Coding challenge

Note: All coding challenges are optional and are not prerequisites for later lessons.

  • The wordlist is just a list of single words, which isn't terribly useful. Extent the app to display definitions, as well as a link to useful information, such as developer.android.com, stackoverflow, or wikipedia.
  • Add an activity that allows users to search for words.
  • Add basic tests for all the functions in WordListContentProvider.

Summary

  • In production, most application developers will typically refactor apps to accommodate a content provider.
  • During refactoring, developers will typically experience cascading changes and errors
  • You need to separate the UI from the database using a content provider and a content resolver
  • The UI should not change during the refactor from an embedded database to an external data source.
  • The Contract class defines the common constants for all the components in the refactored application.
  • The Contract class localizes all the common constants for ease of maintenance.
  • When refactoring, it is very useful to have diagrams of the database access classes
  • Careful thought should be given to designing the database access URIs that other applications need to access the data.
  • All access to your database must be changed to use a Content Resolver instead of directly accessing a helper class (for example: WordListOpenHelper)
  • If the underlying data has changed, it is important to signal the UI to refresh using notifyDataSetChanged().

The related concept documentation is in Android Developer Fundamentals: Concepts.

Learn more

Developer Documentation:

Videos:

results matching ""

    No results matching ""