Content Providers in Android with Example

Content Providers in Android with Example (Complete Guide for Beginners)

Content Providers are one of the core Android components that many beginners skip at first, but they become essential as soon as your app needs to share data with other apps or organize app data in a clean, reusable way. In simple words, a ContentProvider is a standardized interface that exposes a central data store so that other parts of the system, or even other applications, can securely read and write data using a common API.

This complete guide explains what Content Providers are, why Android uses them, their key building blocks like URI and ContentResolver, the main CRUD methods, and a practical example of how to implement a simple custom ContentProvider.

What Is a Content Provider in Android?

A Content Provider is an Android component that manages access to a structured set of data. This data might be stored in an SQLite database, in files, or even fetched from the network, but to the outside world it always looks like a simple table-like data source that can be queried using URIs.

Unlike Activities and Services, a ContentProvider has no user interface. Its main job is to respond to requests from clients via a standard set of methods like query(), insert(), update(), and delete(). Clients do not talk directly to your database; they use a ContentResolver, which in turn communicates with the provider.


Real-World Examples of Content Providers

Android itself ships with several built-in content providers that you use almost daily as a developer without always thinking about it. For example:

  • The Contacts provider that exposes the phone’s contact list.
  • The MediaStore provider that exposes images, audio, and video on the device.
  • The Calendar provider that stores events and reminders.

With these providers, you can query contacts or gallery images from any app using a consistent API, instead of every app inventing its own database format and query mechanism.

When and Why to Use a Content Provider

You do not need a ContentProvider for every piece of data in your app. Many apps use only Room or SQLite databases without exposing them through a provider. You typically use a ContentProvider in these situations:

  • You want to share data with other apps on the device in a controlled and permission-aware way.
  • You want a standard interface so other apps or system components can query, insert, or update your data.
  • You need to integrate with system features that expect a provider pattern, such as widgets or some system UIs.

Inside your own app, you might also use a ContentProvider to centralize data access when multiple modules or processes need to read and write the same data in a uniform way.

Key Concepts: URI, Authority, and ContentResolver

To understand Content Providers properly, three concepts are critical: the content URI, the authority, and the ContentResolver.

Content URI

A content URI is a special kind of URI that uniquely identifies data exposed by a ContentProvider. It usually looks like this:

content://com.example.provider/users
  

This URI has three main parts: the content:// scheme, the authority (for example, com.example.provider), and the path (for example, /users) which points to a specific table or collection.

Authority

The authority is a unique name that identifies your provider on the device, often using your app’s package name. This value is declared in the manifest. Clients use the authority to build URIs that target your provider’s data.

ContentResolver

Clients never call a ContentProvider directly. Instead, they use a ContentResolver, which acts as a middleman. From an Activity or other Context, you obtain it with getContentResolver() and then call methods like query(), insert(), update(), and delete() with the appropriate content URI.

This indirection layer keeps the client code independent from the actual implementation of the provider and ensures permissions and process boundaries are handled correctly by the Android framework.

Core Methods of a Content Provider

When you create a custom ContentProvider, you extend the ContentProvider class and override several key methods. The most important ones are:

  • onCreate() – Called when the provider is first created; use it to initialize your data source (for example, open or create the database).
  • query() – Handles read requests from clients; returns a Cursor containing the requested rows and columns.
  • insert() – Inserts a new row and returns a URI pointing to the newly added item.
  • update() – Updates existing rows that match a selection and returns the number of rows updated.
  • delete() – Deletes rows that match a selection and returns the number of rows removed.
  • getType() – Returns a MIME type string describing the data at the given URI (for example, a directory of rows or a single item).

Together, these methods expose a database-like CRUD interface to the outside world, while you remain free to implement the internals using any storage strategy you prefer.

Creating a Simple Content Provider – Step-by-Step Example

Let’s build a small, easy-to-understand example: a ContentProvider that manages a list of users stored in an SQLite database. Each user has an auto-increment id and a name.

Step 1: Define Contract and URI Constants

It is a good practice to define a contract class that holds constants for table names, column names, and URIs. This keeps both provider and client code consistent.

object UserContract {

    const val AUTHORITY = "com.example.userprovider"
    const val PATH_USERS = "users"

    val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/$PATH_USERS")

    object Columns {
        const val _ID = "id"
        const val NAME = "name"
    }
}
  

Here, AUTHORITY identifies our provider, PATH_USERS represents the user table, and CONTENT_URI is the main URI clients will use to access the users data.

Step 2: Declare the Provider in AndroidManifest.xml

Next, declare the ContentProvider inside your manifest so the system knows about it:

<application
    ...>

    <provider
        android:name=".UserContentProvider"
        android:authorities="com.example.userprovider"
        android:exported="true" />

</application>
  

The android:exported flag controls whether other apps can access your provider. For internal use only, you can set it to false or protect it further with permissions.

Step 3: Implement the UserContentProvider Class

Create a class UserContentProvider that extends ContentProvider. Inside, you will manage a small SQLite database storing user information.

class UserContentProvider : ContentProvider() {

    companion object {
        private const val USERS = 1
        private const val USERS_ID = 2

        private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(UserContract.AUTHORITY, UserContract.PATH_USERS, USERS)
            addURI(UserContract.AUTHORITY, "${UserContract.PATH_USERS}/#", USERS_ID)
        }
    }

    private lateinit var dbHelper: UserDbHelper

    override fun onCreate(): Boolean {
        dbHelper = UserDbHelper(context!!)
        return true
    }

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {

        val db = dbHelper.readableDatabase
        val cursor: Cursor

        when (uriMatcher.match(uri)) {
            USERS -> {
                cursor = db.query(
                    UserDbHelper.TABLE_USERS,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder ?: UserContract.Columns._ID
                )
            }
            USERS_ID -> {
                val id = ContentUris.parseId(uri)
                cursor = db.query(
                    UserDbHelper.TABLE_USERS,
                    projection,
                    "${UserContract.Columns._ID}=?",
                    arrayOf(id.toString()),
                    null,
                    null,
                    sortOrder ?: UserContract.Columns._ID
                )
            }
            else -> throw IllegalArgumentException("Unknown URI $uri")
        }

        cursor.setNotificationUri(context!!.contentResolver, uri)
        return cursor
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val db = dbHelper.writableDatabase

        val rowId = db.insert(UserDbHelper.TABLE_USERS, null, values)
        if (rowId > 0) {
            val insertedUri = ContentUris.withAppendedId(UserContract.CONTENT_URI, rowId)
            context!!.contentResolver.notifyChange(insertedUri, null)
            return insertedUri
        }
        throw SQLException("Failed to insert row into $uri")
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        val db = dbHelper.writableDatabase
        val count: Int = db.update(UserDbHelper.TABLE_USERS, values, selection, selectionArgs)
        if (count > 0) {
            context!!.contentResolver.notifyChange(uri, null)
        }
        return count
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        val db = dbHelper.writableDatabase
        val count: Int = db.delete(UserDbHelper.TABLE_USERS, selection, selectionArgs)
        if (count > 0) {
            context!!.contentResolver.notifyChange(uri, null)
        }
        return count
    }

    override fun getType(uri: Uri): String {
        return when (uriMatcher.match(uri)) {
            USERS -> "vnd.android.cursor.dir/vnd.${UserContract.AUTHORITY}.users"
            USERS_ID -> "vnd.android.cursor.item/vnd.${UserContract.AUTHORITY}.users"
            else -> throw IllegalArgumentException("Unknown URI $uri")
        }
    }
}
  

This provider supports querying all users or a single user by ID, as well as inserting, updating, and deleting rows. It also notifies observers whenever data changes so that UI components can refresh automatically if they are using a CursorAdapter or similar pattern.

Step 4: SQLiteOpenHelper for the Users Table

The provider relies on a helper class to create and upgrade the SQLite database. Here is a minimal implementation:

class UserDbHelper(context: Context) :
    SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

    override fun onCreate(db: SQLiteDatabase) {
        val createTable = """
            CREATE TABLE $TABLE_USERS (
                ${UserContract.Columns._ID} INTEGER PRIMARY KEY AUTOINCREMENT,
                ${UserContract.Columns.NAME} TEXT NOT NULL
            )
        """.trimIndent()
        db.execSQL(createTable)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.execSQL("DROP TABLE IF EXISTS $TABLE_USERS")
        onCreate(db)
    }

    companion object {
        const val DATABASE_NAME = "users.db"
        const val DATABASE_VERSION = 1
        const val TABLE_USERS = "users"
    }
}
  

With this helper in place, your ContentProvider can safely read and write data using an internal SQLite database while exposing a clean, URI-based API to clients.

Using the Content Provider from an Activity

Clients access your data through a ContentResolver. For example, to insert a new user from an Activity:

val values = ContentValues().apply {
    put(UserContract.Columns.NAME, "John Doe")
}

val uri = contentResolver.insert(UserContract.CONTENT_URI, values)
  

To query all users:

val cursor = contentResolver.query(
    UserContract.CONTENT_URI,
    arrayOf(UserContract.Columns._ID, UserContract.Columns.NAME),
    null,
    null,
    null
)

cursor?.use {
    while (it.moveToNext()) {
        val id = it.getLong(it.getColumnIndexOrThrow(UserContract.Columns._ID))
        val name = it.getString(it.getColumnIndexOrThrow(UserContract.Columns.NAME))
        // Use id and name (for example, log or display in a RecyclerView)
    }
}
  

From the Activity’s point of view, this looks similar to querying a database directly, but the actual storage is abstracted behind the provider. This allows other apps, services, and widgets to reuse the same interface if you choose to export it.

Best Practices for Content Providers

To design robust and secure providers, keep the following best practices in mind:

  • Use clear, well-structured URIs and document them if you expect other apps to consume them.
  • Protect sensitive data using permissions, non-exported providers, or custom URI permissions.
  • Return meaningful MIME types from getType(), distinguishing between single items and collections.
  • Always call notifyChange() when data changes so that observers can react correctly.
  • Handle errors gracefully and throw clear exceptions for unsupported URIs or invalid operations.

Conclusion

Content Providers in Android offer a powerful abstraction for managing and sharing structured data. By exposing a uniform CRUD interface over URIs, they allow both your own app and external apps to interact with data sources in a safe, decoupled way.

In this guide, you learned what ContentProviders are, why they are useful, how URIs and ContentResolvers fit into the picture, and how to implement a complete example with an SQLite-backed user table. With these concepts and patterns, you can confidently build providers for contacts, notes, media, or any other data your Android applications need to share or centralize.

Post a Comment

0 Comments