Android Content Provider in Kotlin

Welcome, here we are going to learn about one of the android components: Content Provider.

What is a Content Provider?

When we need to access the data of other application or when we need to make our application data available for other applications to access securely, Content Provider comes in the scene. We have a class called ContentResolver, which helps us to manage the requests of data. The data can be a stored file, a database or maybe over a network. Content Provider responds with the data in the cursor format.

Let’s take a real-time example to understand the Content Provider. We all use WhatsApp application. The list of contact which we save get synced with WhatsApp according to the user using WhatsApp or not. But, how the WhatsApp is accessing our contacts from our phones? The answer is — Using the Content Provider.

So, our contact application has a Content Provider which shares it’s data to other application and WhatsApp request to the contact app to get access of list of contacts. It makes the contact app a provider and WhatsApp as a client which will use the data provided by the provider.

Let’s learn some of the essential terminologies of Content Provider.

Content URI (Uniform Resource Identifier) — We can understand URI as a path of the data.

content://authority/optionalPath/optionalId

content:// — It is a scheme of Content Provider which always presents.

authority — It is a name of the Content Provider which is unique like browser, contacts etc.

optionalPath — It is a path which helps to differentiate our data in the content provider.

optionalId — It is a single record of a file if we want to access something specific from the content provider. This optionId must be number.

There are two types of URI:

  1. Directory-based — if there is no id in URI
  2. Id based — if there is an id in URI

query() — This method is from the Content Resolver class, helps us to query the data and return a cursor

This method has some parameters:

Projection — It is an array of the columns from the data table which we want to get in our result when we query the Content Provider

Selection — It is a condition for selecting the rows from the data table

SortOrder — It is the order of the data which returns when we query

mCursor = contentResolver.query(
        CONTENT_URI,   // The content URI of the words table
        Projection,    // The columns to return for each row
        SelectionClause,            // Selection criteria
        SelectionArgs.toTypedArray(),  // Selection criteria
        SortOrder     // The sort order for the returned rows
)

Cursor — This data which we will get after query the Content Provider, is in Cursor type. Cursor class helps the app to manage the information

Sometimes the cursor may be null, best practice to check the nullability

// iterate the cursor
when (mCursor?.count) {
}
// iterate the cursor
mCursor?.apply {
   while (moveToNext()) { 
      // to iterate the cursor 
    }
}

CursorLoader — This uses Content Provider to run a query against the database and returns Cursor

SimpleCursorAdapter — This helps to map the data from the cursor to UI

ContentValues — This use to store the set of values which Content Resolver class can process

val newValues = ContentValues().apply {
    // Sets the values of each column and inserts the value. 
     // The arguments to the "put"
     // method are "column name" and "value"
     
    put(COLUMN_NAME, YOUR_VALUE)
}

Loaders — This helps to load the data from Content Provider to UI

<Provider> — To declare that our app uses Content Provider we need to report it in a manifest file using this tag

<provider
        android:name=".MyProvider"
      android:authorities="com.mindorks.MyProvider.AUTHORITY"
/>

Contract Class — This is a class where we need to define the constant definitions URIs, column names, MIME types, and other meta-data about the Content Provider. It can also contain static helper methods to manipulate the URIs.

Uri Matcher — Which action to take on an incoming request (content URI), URI Matcher class plays a role here. This return as an integer valueA content URI pattern matches content URIs using these two characters:

  • *: Matches a string of any valid characters of any length
  • #: Matches a string of numeric characters of any length

Let’s develop a simple application where we will insert the data and query it using content resolver and display on the screen.

Firstly, we need to create the layout of our main screen where we will display our data.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
   <TextView
        android:id="@+id/tvDisplayDataHere"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@+id/tvDisplayAll"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
   <Button
        android:id="@+id/tvDisplayAll"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginBottom="16dp"
        android:onClick="onClickDisplayEntries"
        android:text="Display All"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/tvDisplayFirst"
        app:layout_constraintStart_toStartOf="parent" />
   <Button
        android:id="@+id/tvDisplayFirst"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:onClick="onClickDisplayEntries"
        android:text="Display First Item"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/tvDisplayAll"
        app:layout_constraintTop_toBottomOf="@+id/tvDisplayDataHere"
        app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>

Add the list of string in the strings.xml file

<string-array name="sentence">
    <item>Android</item>
    <item>ContentProvider</item>
    <item>With</item>
    <item>MindOrks</item>
</string-array>

Now, we will create a contract class. All are variable should be final static, so as per kotlin we are using companion object.

class Contract  {
   companion object {
val AUTHORITY  =     "com.mindorks.mindorkscontentprovider.provider"
val CONTENT_PATH = "sentence"
val CONTENT_URI = Uri.parse("content://$AUTHORITY/$CONTENT_PATH")
       val ALL_ITEMS = -2
        val WORD_ID = "id"
    }
}

Now, Let’s create our Content Provider class

class MindOrksContentProvider : ContentProvider() {
   var mData: Array<String>? = null
   private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH)
   override fun onCreate(): Boolean {
        initializeUriMatching()
        val context = context
        mData = context!!.resources.getStringArray(R.array.sentence)
        return true
    }
   private fun initializeUriMatching() {
        sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH + "/#", 1)
        sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH, 0)
    }
   override fun query(uri: Uri, projection: Array<String>?, selection: String?,
                       selectionArgs: Array<String>?, sortOrder: String?): Cursor {
        var id = -1
        when (sUriMatcher.match(uri)) {
            0 -> {
                id = Contract.ALL_ITEMS
                if (selection != null) {
                    id = Integer.parseInt(selectionArgs!![0])
                }
            }
            1 -> id = parseInt(uri.getLastPathSegment())
            UriMatcher.NO_MATCH -> {
                id = -1
            }
            else -> {
                id = -1
            }
        }
        return populateCursor(id)
    }
   private fun populateCursor(id: Int): Cursor {
        val cursor = MatrixCursor(arrayOf(Contract.CONTENT_PATH))
        if (id == Contract.ALL_ITEMS) {
            for (i in 0 until mData!!.size) {
                val word = mData!![i]
                cursor.addRow(arrayOf<Any>(word))
            }
        } else if (id >= 0) {
            val word = mData!![id]
            cursor.addRow(arrayOf<Any>(word))
        }
        return cursor
    }
   override fun getType(uri: Uri): String? {
        return null
    }
   override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return null
    }
   override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        return 0
    }
   override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
        return 0
    }
}

Let’s connect with our MainActivity

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
   fun onClickDisplayEntries(view: View) {
        tvDisplayDataHere.text = ""
        var queryUri = Contract.CONTENT_URI.toString()
        var projection = arrayOf(Contract.CONTENT_PATH)
        var selectionClause: String?
        var selectionArgs: Array<String>? = null
        var sortOrder: String? = null
       when (view.id) {
            R.id.tvDisplayAll -> {
                selectionClause = null
                selectionArgs = null
            }
            R.id.tvDisplayFirst -> {
                selectionClause = Contract.WORD_ID + " = ?"
                selectionArgs = arrayOf("0")
            }
            else -> {
                selectionClause = null
                selectionArgs = null
            }
        }
       val cursor = contentResolver.query(Uri.parse(queryUri), projection, selectionClause,
                selectionArgs, sortOrder)
       if (cursor != null) {
            if (cursor.count > 0) {
                cursor.moveToFirst()
                val columnIndex = cursor.getColumnIndex(projection[0])
                do {
                    val word = cursor.getString(columnIndex)
                    tvDisplayDataHere.append(word + "\n")
                } while (cursor.moveToNext())
            } else {
                tvDisplayDataHere.append("No data returned." + "\n")
            }
            cursor.close()
        } else {
            tvDisplayDataHere.append("Cursor is null." + "\n")
        }
    }
}

Lastly, add the provider declaration to the manifest file

<provider 
android:name=".MindOrksContentProvider" android:authorities="com.mindorks.mindorkscontentprovider.provider" />

Let’s run the application. Clicking on each button will give you the required result — great work.

Now, from the next chapters, we are going to learn about the database in Android.