Making Use of Android Content Providers

in #utopian-io7 years ago (edited)

Repository

https://github.com/aosp-mirror/

What Will I Learn?

  • You will learn what Android content providers are all about
  • You will learn how to create and implement an Android Content Provider in your application
  • You will learn the benefits of using the Android Content Provider

Requirements

  • Android Studio 2.3 and above
  • Basic Knowledge of native Android development using Java

Difficulty

  • Intermediate

Tutorial Contents


Image Source

The Content Provider class is one of the four fundamental components of the Android framework, the others being the commonly used Activity class, the Broadcast Receivers and Services. It is a very important class that sits as an abstract layer over the raw SQLite storage framework in android. To learn more about the SQLite android framework check out this previous tutorial i did. While the raw SQLite classes can be used to store data in android, a number of problems is posed by using this approach, some of which include

  • Inability to share data from your application with other applications
  • A lot of stress in carrying out asynchronous queries using Loaders
  • Many instances of the SQLite storage class is created when using it which leads to memory issues and mismanagement of android's limited resources.

Due to these issues amongst others, the content provider was added to android's framework to handle all these by providing a layer over the raw SQLite and ensuring a better code structure for your android application.

Adding the Content Provider

To get started with using the content provider, you have to create a class that extends from the ContentProvider class in your android studio project. This class is an abstract class and thus has about 4 methods that must be implemented

  • insert()
  • query()
  • update()
  • delete()
    which represent the CRUD(CREATE READ UPDATE DELETE) operations that can be performed via the content provider. Also, lets create another class called Constants that holds a reference to static final variables that would be needed to use our content provider, here's a code snippet for this class below
public class Constants{
public static final String DATABASE_TABLE = "MyApp";
public static final String APP_AUTHORITY = "com.android.myapp";
public static final Uri CONTENT_BASE_URI = Uri.parse("content://" + APP_AUTHORITY + "/MyApp");

}

It is better to use these constants as they can be called anywhere in the app without worrying about the possibility of a mistake or error in misspelling in the string. The DATABASE_TABLE string is the name of our table that has been created using raw SQL syntax. The next string called APP_AUTHORITY is usually the package name of the app and used to create the next constant of type URI which is parsed using the it and is simply a constant that helps android to recognize the content provider it should call and point to a single row or a whole table in the database.

The content uri consists of 3 parts: The Scheme, the Content Authority and the Type of Data that is being processed from the database. The scheme is the part of the CONTENT_BASE_URI string ("content://") which specifies that the uri is of type content provider. The authority is used to register the content provider in our manifest file and specifies the class which holds the content provider and finally we have the Type of Data which specifies if the whole or just part of the database table should be operated on by the content provider.

Creating the content uri string as shown above in our CONTENT_BASE_URI means it can be used to query the whole table, while to query a specific row in the table, it is further appended with the id of the row to be queried, for example to query row with id 5 we simply use,

private int rowId = 5;
public static final Uri CONTENT_BASE_URI = Uri.parse("content://" + APP_AUTHORITY + "/MyApp/" + rowId);

Now we have to specify which part of the table is referred to by the uri by using integer constants mapped to the uri patterns, so for uri matching the full table, lets map to an integer 200 while for a uri matching a specific row in the table we use 400, code is shown below,

private static final int CONTENT_MATCH_TABLE = 200;
private static final int CONTENT_MATCH_TABLE_ROW = 400;

You can use any value of integer for this mapping but make sure the values are unique. Moving on, to specify the part of the table to be operated on, these integers declared above are used by a class called UriMatcher. The UriMatcher class is an android class that helps to match URIs and respond in a specific predetermined way. It actually does the main role of sorting the passed in URI from anywhere in the app into either a URI that queries the whole table or one that concerns just a specific row.

Now lets create our Content provider class and implement our UriMatcher, a class called MyProvider is created that extends from ContentProvider and added to the android manifest file since it is a fundamental component of android as shown below,

<provider
            android:authorities="com.android.myapp"
            android:name=".MyProvider"
            android:exported="true">

</provider>

This shows the authority of the content provider under the provider tag. The name attribute is the created class that extends ContentProvider and the exported attribute indicates if you want your content provider to be used to share data with external applications.
Now lets modify the MyProvider class as follows,

public class MyProvider extends ContentProvider{
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

private MyDatabase myDatabase;

static{
uriMatcher.addURI(Constants.APP_AUTHORITY,Constants.DATABASE_TABLE , CONTENT_MATCH_TABLE );
uriMatcher.addURI(Constants.APP_AUTHORITY,Constants.DATABASE_TABLE +"/#", CONTENT_MATCH_TABLE_ROW );
}

@Override
    public boolean onCreate() {
        myDatabase = new MyDatabase(getContext());
        return true;
}


    @Nullable
    @Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
//Perform queries operations
}

@Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
}

    @Nullable
    @Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
//perform insert operations
}

@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
//perform delete operations
}

 @Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        //perform delete operations
        return 0;
}

}

The class overrides some methods as shown above, the 4 CRUD methods outlined earlier and the onCreate() method which is where the provider is first instantiated and the getType() method which is used to find out what type of data a content uri is referring to.

In the class above, an instance global variable of the raw SQLite database class used to store the data is created in the onCreate() method of the MyProvider class. A global instance of the UriMatcher class is created and it is used in a static constructor block to add the URIs that it should distinguish. The uriMatcher.addUri() takes in 3 parameters, the app authority string, the type of data to be operated on in the database: that is, either the whole table or a specific row in the table which is indicated by appending a pound sign placeholder depicting that the uri can accept any integer to specify data in a particular row and finally the integer to be returned by the matcher(that is, the static final integers we created earlier for mapping).

The myDatabase variable can then be used in any of the CRUD methods to perform the corresponding CRUD operations on the database. These CRUD methods call the corresponding CRUD methods from the database thus performing the required operation. So to perform a query, we have to use the uriMatcher to determine which uri of the content provider is matched to the uri that is passed in via the query method. This is shown below,

   @Nullable
    @Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
SQLiteDatabase database = myDatabase.getReadableDatabase();
        Cursor cursor;
        int match = uriMatcher.match(uri);
        switch (match){
            case CONTENT_MATCH_TABLE :
                cursor = database.query(Constants.DATABASE_TABLE, projection, selection,
                        selectionArgs,null,null,sortOrder);
                return cursor;
            case CONTENT_MATCH_TABLE_ROW :
                //write row specific code
                break;
            default:
                //write code for when the uriMatcher is not able to match the URIs
        }
return null;

}

The uriMatcher variable is used to match the uri passed in via the query() method by calling uriMatcher.match(uri) which returns a unique code that is either one of the two initially declared integers(200 or 400). This integer is then used in a switch statement to determine which part of the database is queried. If the uri matcher matches for the whole table, then the database object calls the query method passing in the table name and the projections as the remaining parameters from the query() method of the content provider into the query method of the database object.

A cursor object is returned after the querying and returned from the query method. The cursor object holds the data that was demanded by the user for display in the UI. The projections simply represent a way to organize/sort the data the cursor object returns.

To delete an item from the database, the same procedure is followed, using the uriMatcher and depending on the matched uri, the database object delete() method is called as shown below,

 @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        SQLiteDatabase database = myDatabase.getWritableDatabase();
        int match = uriMatcher.match(uri);
        if (match == CONTENT_MATCH_TABLE ){
            database.delete(Constants.DATABASE_TABLE, null,null);
        }
        if (match == CONTENT_MATCH_TABLE_ROW ){
            database.delete(Constants.DATABASE_TABLE,selection,selectionArgs);
        }
        if (match == UriMatcher.NO_MATCH) {

Toast.makeText(getContext(), "Cannot query unknown uri", Toast.LENGTH_SHORT).show();

}
        return 0;
}

The URI matcher does the matching, and if the whole table is to be deleted calls the delete() method passing in the table name and null as the remaining parameters so as to delete the whole table. If a row is to be deleted the same method is called, passing in the table name and the projections of the row to be deleted such as row number. If no URI is matched, then a toast is displayed to the user. The delete method returns an integer of the row deleted.

To perform an insertion of data on the database, implement the same procedure as shown below,

 @Nullable
    @Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
SQLiteDatabase database = myDatabase.getWritableDatabase();
        int match = uriMatcher.match(uri);
        if (match == CONTENT_MATCH_TABLE ){
            database.insert(Constants.DATABASE_TABLE,null, values);
        }
        if (match == UriMatcher.NO_MATCH)Toast.makeText(getContext(), "Cannot query unknown uri", Toast.LENGTH_SHORT).show();
return null;
}

The insert method takes in the uri and content values. Content values hold the object details to be inserted into the database. The UriMatcher does the matching and insertion is done using the database object. If no uri can be matched then a toast is displayed. The update method is performed in the same way.

Calling The Content Provider

To use the content provider, it can be called from anywhere in the application using a variable from an android class called ContentResolver. This class has a public method getContentResolver() which is inherited by context classes(Activities and fragments) that returns a ContentResolver which is used to call the 4 CRUD methods of the content provider, passing in the necessary parameters.

So, to query the whole table, in any activity in your project you simply call the resolver as follows,

String columnName = "Some Column Name";
 Cursor cursorApp = getContentResolver().query(Constants.CONTENT_BASE_URI,columnName,null,null,null);
//use cursor to populate UI or as required

The query method is called, passing in the uri that matches the whole table and projection values of null. This query method returns a null which can then be used as required. Similarly to insert data, we first put the data in a ContentValues object and then pass this object along with the URI for insertion into the insert() method. This is shown below,

ContentValues contentValues = new ContentValues();
contentValues.put("ColumnName",Value of data to be inserted);
contentValues.put("ColumnName",Value of data to be inserted);
contentValues.put("ColumnName",Value of data to be inserted);
contentValues.put("ColumnName",Value of data to be inserted);
 getContentResolver().insert(Constants.CONTENT_BASE_URI, contentValues);

To delete data from the whole table, the steps are similar, shown below is the code,

getContentResolver().delete(Constants.CONTENT_BASE_URI, null, null);

To delete data from a specific row item when it is clicked in a ListView or RecyclerView, we implement as follows,

String position = String.valueOf(clickedPosition);
Uri appendedUri = Uri.withAppendedPath(Constants.CONTENT_BASE_URI,position);
getContentResolver().delete(appendedUri,null,
+                null);

The clicked item's position is first converted to a string using the String.valueOf() method. It can double as the id of the row for which deletion should occur. It is then appended to the CONTENT_BASE_URI string to create a uri of the form that would be matched by the UriMatcher to a specific row. This appending is done with the Uri.withAppendedWith() method passing in the base URI and the position to be appended. This appended URI is then passed into the getContentResolver().delete() method along with any extra projections.

Conclusion

As can be seen, the content provider helps in using an abstraction layer to prevent direct interaction with the SQLite database. This helps in better code structure and it can also be used to share data from an app with another app using the URIs defined in the content provider. However, the content provider still has shortcomings of its own. Apart from the fact that it is quite tasking to write and implement a content provider, it doesn't eliminate the problem of Runtime checking of errors which means that any wrong step taken during its creation and the app will crash at runtime.

To solve this, we will look at another method of storage in android known as Room Persistence library, which is also an abstraction layer over the SQLite framework, in the next tutorial of this series.

Thank you for reading, i hope this helped you learn one of android's 4 core components.

Curriculum

Proof of Work Done

To learn more on the Android Content Provider check out code from an application i developed using it via the link
https://github.com/demistry/CryptoCur_App/blob/aec0aca3274ff63ba97ce10c85d48e5e86c6f722/app/src/main/java/com/android/cryptocurapp/storage/CryptoCurContentProvider.java

Link to full application code is https://github.com/demistry/CryptoCur_App

Sort:  

Thanks for the contribution, I'd suggest you to use markdowns a bit more to make your contribution more readable, like focus on where we should etc.

Your contribution has been evaluated according to Utopian rules and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post,Click here


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Thank you very much. I will certainly put that into practice

Hey @davidemi
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Contributing on Utopian
Learn how to contribute on our website or by watching this tutorial on Youtube.

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!