4.
ContentProvider
Written by Fuad Kamal
Being able to persist structured data in a SQLite database is an excellent feature included in the Android SDK. However, this data is only accessible by the app that created it. What if an app would like to share data with another app? ContentProvider is the tool that allows apps to share persisted files and data with one another.
Content providers sit between the app’s data source and provide a means to manage this data. This can be a helpful organizational tool in an app, even if the app is not intended to share its data externally with other apps. Content providers provide a standardized interface that can connect data in one process with code running in another process. They encapsulate the data and provide mechanisms for defining data security at a granular level. A content provider can be used to aggregate multiple data sources and abstract away the details.
Although it can be a good idea to use a content provider to better organize and manage the data in an app, this isn’t a requirement if the app isn’t going to share its data.
One of the simplest use cases of a content provider is to gain access to the Contacts of a device via a content provider. Another common built-in provider in the Android platform is the user dictionary. The user dictionary holds spellings of non-standard words specific to the user.
Understanding content provider basics
In order to get data from a content provider, you use a mechanism called ContentResolver. It provides methods to query, update, insert and delete data from a content provider. A request to a content resolver contains an URI to one of the SQL-like methods. These methods return a Cursor instance.
Note: Cursor is defined and discussed in more detail in the previous SQLite chapter. It’s essentially a pointer to a row in a table of structured data that was returned by the query.
There are two basic steps to interact with a content provider via content resolver:
- Request permission from the provider by adding a permission in the manifest file.
- Construct a query with an appropriate content URI and send the query to the provider via content resolver object.
Understanding Content URIs
To find the data within the provider, use a content URI. The content URI is essentially the address of where to find the data within the provider. A content URI always starts with content://
and then includes the authority of a provider which is the provider’s symbolic name. It can also include the names of tables or other specific information relating the query. An example content URI for the user dictionary looks like:
content://user_dictionary/words
Oftentimes, providers allow you to append an ID value to the end of the URI to find a specific record, or a string such as count to denote that you want to run a query that counts the number of records. You must refer to the provider documentation to figure out what a specific content provider exposes.
Requesting permission to use a content provider
The application will need read access permission for the specific provider. Utilize the <uses-permission>
element and the exact permission that was defined by the provider. The provider’s application can specify which permissions requesting applications must have in order to access the data. Users can see the requested permissions when they install the application. For example, the code to request read permission of the user dictionary is:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
If you want to use the user dictionary, the above permission should be added inside the manifest tag in AndroidManifest.xml.
Permission types
The types of permission that can be granted by a content provider include:
- Single read-write provider-level permission – This permission controls both read and write access to the whole provider. It’s one permission to rule them all. :]
- Separate read and write provider-level permission – Read and write permissions can be set separately for the whole provider.
- Path-level permission – Read, write or read/write permissions can be applied to each content URI individually.
- Temporary permission – Temporary access can be granted to an application even if it doesn’t have the permissions that would otherwise be required. This means that only applications that need permanent permissions for your providers are apps that continually access your data.
Constructing the query
The statement to perform a query on the user dictionary database looks like this:
cursor = contentResolver.query(
// 1
UserDictionary.Words.CONTENT_URI,
// 2
projection,
// 3
selectionClause,
// 4
selectionArgs,
// 5
sortOrder
)
contentResolver
that is part of a context is utilized to call the query function and a list of arguments can be passed if necessary. The only required argument is the content URI. Below is an explanation of each of the parameters.
- The content URI of the provider including the desired table.
- The columns definitions to return for each row, this is
String
array. - The selection clause, similar to the WHERE clause only excluding the where. A
?
is used in place of arguments. - The arguments to be utilized for the selection clause that fill in the
?
. - The sort order such as
"ASC"
or"DESC"
.
Note: Allowing raw SQL statements from external sources can lead to malicious input from SQL injection attempts. Using the selection clause with
?
representing a replaceable parameter and an array of selection arguments instead can prevent the user from making these attempts.
A content provider not only allows data to be read by an outside application, the data can also be updated, added to or deleted.
Inserting, updating and deleting data
The insert, update and delete operations look very similar to the query operation. In each case, you call a function on the content resolver object and pass the appropriate parameters in.
Inserting data
Below is a statement to insert a record:
newUri = contentResolver.insert(
UserDictionary.Words.CONTENT_URI,
newValues
)
newValues
is a ContentValues object which is populated with key-value pairs containing the column and value of the data to be inserted into the new row. newURI
contains the content URI of the new record in the specific form — content://user_dictionary/words/<id_value>
.
Updating data
To update data, call update()
on the content resolver object and pass in content values that include key-values for the columns being updated in the corresponding row. Arguments should also be included for selection criteria and arguments to identify the correct records to update. When populating the content values, you only have to include columns that you’re updating, and including column keys with a null
value will clear out the data for that column. One important consideration when updating data is to sanitize user input. The developer guide to protecting against malicious data has been included in the Where to go from here? section below. update()
returns an Integer
value that contains the count of how many rows were updated.
Deleting data
Deleting data is very similar to the other operations. Call delete()
on the content resolver object passing in arguments for the selection clause and selection arguments to identify the group of records to delete. delete()
returns an Integer
value which represents the count of how many rows were deleted.
Adding a contract class
A contract class is a place to define constants used to assemble the content URIs. This can include constants to contain the authority, table names and column names along with assembled URIs. This class must be created and shared by the developer creating the provider. It can make it easier for other developers to understand and utilize the content provider in their application.
MIME types
Content providers can return standard MIME types like those used by media, or custom MIME type strings, or both. MIME types take the format type/subtype
an example being text/html
. Custom MIME types or vendor-specific MIME types are more complicated and come in the form of vnd.android.cursor.dir
for multiple rows, and vnd.android.cursor.item
for single rows.
The vnd stands for vendor and isn’t part of the package name of the app. The subtype of a custom MIME type is provider-specific and is generally defined in the contract class for the provider.
getType()
of the provider returns a String
in MIME format. If the provider returns a specific type of data, getType()
returns the common MIME type for that data. If the content URI points to a row or a table of data, getType()
returns a vendor specific formatted MIME type including the authority and the table name, e.g. vnd.android.cursor.dir/vnd.com.raywenderlich.android.contentprovidertodo.provider.todoitems
. This MIME type is for multiple rows in the todoitems table with the authority com.raywenderlich.android.contentprovidertodo.provider.
A content URI can also perform pattern matches using content URIs that include wildcard characters.
-
*
– Matches aString
object of any valid characters of any length. -
#
– Matches aString
object of numeric characters of any length.
Now that you’ve learned a little bit about content providers, it’s time to create one of your own.
Getting started
Locate the content-provider folder and open up the projects folder inside of it. Next, open the ContentProviderToDo app under the starter folder. Allow the project to sync, download dependencies, and setup the workplace environment. Build and run to see the main screen.
You have the shell for a basic todo list app, but if you try to add anything to the todo list now, nothing gets added.
The provider package
It’s a good idea to keep the provider classes in their own package. If you look inside the controller
package, you’ll find the provider
package. It contains the file with a ToDoContract
class.
The contract class
Now, open ToDoContract.kt and examine the contents:
// The ToDoContract class
object ToDoContract {
// 1
// The URI Code for All items
const val ALL_ITEMS = -2
// 2
//The URI suffix for counting records
const val COUNT = "count"
// 3
//The URI Authority
const val AUTHORITY = "com.raywenderlich.android.contentprovidertodo.provider"
// 4
// Only one public table.
const val CONTENT_PATH = "todoitems"
// 5
// Content URI for this table. Returns all items.
val CONTENT_URI = Uri.parse("content://$AUTHORITY/$CONTENT_PATH")
// 6
// URI to get the number of entries.
val ROW_COUNT_URI = Uri.parse("content://$AUTHORITY/$CONTENT_PATH/$COUNT")
// 7
// Single record mime type
const val SINGLE_RECORD_MIME_TYPE = "vnd.android.cursor.item/vnd.com.raywenderlich.android.contentprovidertodo.provider.todoitems"
// 8
// Multiple Record MIME type
const val MULTIPLE_RECORDS_MIME_TYPE = "vnd.android.cursor.item/vnd.com.raywenderlich.android.contentprovidertodo.provider.todoitems"
// 10
// Table Constants
object ToDoTable {
// The constants for the table columns
object Columns {
//The unique ID column
const val KEY_TODO_ID: String = "todoid"
//The ToDo's Name
const val KEY_TODO_NAME: String = "todoname"
//The ToDo's category
const val KEY_TODO_IS_COMPLETED: String = "iscompleted"
}
}
}
- The ALL_ITEMS constant is used for the URI when
query()
returns all the items in the database. - count is the suffix used on the URI when the count of items in the table is requested.
- AUTHORITY is the prefix of the URI that serves as the symbolic name for the provider.
- CONTENT_PATH corresponds to the name of the todoitems table.
- CONTENT_URI is created by concatenating the authority, or name of the provider with the path, or the name of the table. Then it’s parsed into a URI used to get all the records from the provider.
- ROW_COUNT_URI is a second URI that utilizes the same authority but has the count content type. It’s used to retrieve the number of records in the table.
- SINGLE_RECORD_MIME_TYPE is the complete, custom MIME type for URIs that return a single record.
- MULTIPLE_RECORD_MIME_TYPE is the custom MIME type for URIs that will return multiple records. Notice the use of dir instead of item — item is used for a single record MIME type.
- ToDoTable is an inner object that contains the name of the main table and definitions for the columns in the database.
ToDoContract
is the Contract class that contains all the constant definitions you need for your content provider. This class can be distributed to client apps that would like to use the provider and will provide insight into what this provider has to provide.
Adding the content provider
Android Studio has a neat feature to automatically add content classes. A content provider class extends ContentProvider from the Android SDK and implements all the required methods. By using the automated method of adding the content provider class, many of these method stubs will be provided for you. It will be your job to fill in the functions in the content provider one by one. Ready to get started? :]
Right click the provider package and select New > Other > Content Provider. Provide the class name ToDoContentProvider and the URI authority com.raywenderlich.android.contentprovidertodo.provider. Also check the boxes for exported and enabled. Ensure that the source language is Kotlin, and press Finish.
The class name field provides the name of the new class that will be created in the provider package. The URI Authorities field can contain a semicolon-separated list of one to many URI authorities that identify the data under the purview of the content provider. Checking the exported field means that the component can be used by components of other applications. This automatically adds a <Provider>
element into AndroidManifest.xml. Checking the enabled field allows the component to be instantiated by the system.
Now that you have added the template for your content provider, open AndroidManifest.xml to see the provider tag that has been added.
<provider
android:name=".controller.provider.ToDoContentProvider"
android:authorities="com.raywenderlich.android.contentprovidertodo"
android:enabled="true"
android:exported="true" />
Notice how the location, authority and permissions are included in this tag as we specified through the dialog. Next, you need to implement the methods of the content provider.
Implementing the content provider methods
Note: As you add code to the method stubs in the provider, be sure to replace the TODO comments with the new code. Also, you may need to press alt + enter and import libraries as you go along. If given the choice between constants defined in the ToDoDbSchema or the new ToDoContract, choose ToDoContract. The goal is to have the content provider depending on the contract so that it serves as an abstract layer above the database handler. This design allows for the data source to be swapped out as long as it meets the same specifications as the previous data source, and no other code that is dependent on the database will be affected, in this app or other apps that utilize the contract.
Open ToDoContentProvider.kt. Notice how the class inherits from ContentProvider
in the Android SDK. Before implementing the methods, add the following declarations inside the class above all the overridden method stubs:
// 1
// This is the content provider that will
// provide access to the database
private lateinit var db : ToDoDatabaseHandler
private lateinit var sUriMatcher : UriMatcher
// 2
// Add the URI's that can be matched on
// this content provider
private fun initializeUriMatching() {
sUriMatcher = UriMatcher(UriMatcher.NO_MATCH)
sUriMatcher.addURI(AUTHORITY,CONTENT_PATH, URI_ALL_ITEMS_CODE)
sUriMatcher.addURI(AUTHORITY, "$CONTENT_PATH/#", URI_ONE_ITEM_CODE)
sUriMatcher.addURI(AUTHORITY, "$CONTENT_PATH/$COUNT", URI_COUNT_CODE)
}
// 3
// The URI Codes
private val URI_ALL_ITEMS_CODE = 10
private val URI_ONE_ITEM_CODE = 20
private val URI_COUNT_CODE = 30
- Add an instance of the database handler as the content provider sits between the database and the rest of the program, also create a URI matcher to help construct the URIs.
- Create
initializeURIMathching()
and add each URI that this content provider can match with. This provider will accommodate a URI to retrieve a single record with an id hence the#
, a URI to get all the records, and a URI to get a count of the number of records. Each URI includes the authority, the content path, any arguments represented by special characters, and a unique code. - Declare some constants that represent the numeric code for the URI. The contract class gives descriptive names to the corresponding values these codes will match with. These codes are unique and chosen by the developer.
The next step is to start implementing the stub methods one by one. The template doesn’t always put them in the most logical order by default. You can reorder them if you like.
Initializing the database and Uris
Insert the code below into onCreate()
stub replacing TODO
:
context?.let {
db = ToDoDatabaseHandler(it)
// intialize the URIs
initializeUriMatching()
}
return true
onCreate()
prepares the content provider by instantiating the database handler object, calling initializeUriMatching()
to initialize the URI matcher, and returns true
to signal that this content provider was created successfully.
Resolving the MIME type
Next, implement getType()
by replacing the entire stub with the code below:
override fun getType(uri: Uri) : String? = when(sUriMatcher.match(uri)) {
URI_ALL_ITEMS_CODE -> MULTIPLE_RECORDS_MIME_TYPE
URI_ONE_ITEM_CODE -> SINGLE_RECORD_MIME_TYPE
else -> null
}
This function accepts Uri
and matches it with the URI code. Then it returns the correct MIME type. These constants are defined in the contract that has made the code for this function more self-descriptive.
Now that the trivial details are out of the way, you can jump into implementing the CRUD operations.
Querying the database
query()
queries the database and returns the results. It has been designed so that it can perform multiple types of queries, depending on the URI. Insert the code below into the body of the function:
var cursor : Cursor? = null
when (sUriMatcher.match(uri)) {
URI_ALL_ITEMS_CODE -> { cursor = db.query(ALL_ITEMS)}
URI_ONE_ITEM_CODE -> {
uri.lastPathSegment?.let {
cursor = db.query(it.toInt())
}
}
URI_COUNT_CODE -> { cursor = db.count()}
UriMatcher.NO_MATCH -> { /*error handling goes here*/ }
else -> { /*unexpected problem*/ }
}
return cursor
Here you declare a Cursor
object and assign it to null
. Based on the provided uri
, the URI matcher returns the corresponding code. Then, the WHEN statement calls the correct function on the database handler object to retrieve the results, assigns them to the cursor
and returns cursor
.
Modifying the adapter
To test the content provider you just created, open ToDoAdapter.kt and add the following code inside the class before onCreateViewHolder()
:
private val queryUri = CONTENT_URI.toString() // base uri
private val queryCountUri = ROW_COUNT_URI.toString()
private val projection = arrayOf(CONTENT_PATH) //table
private var selectionClause: String? = null
private var selectionArgs: Array<String>? = null
private val sortOrder = "ASC"
These declarations provide the parameters you’ll need to pass to the contentResolver
methods to query, update, insert and delete from the database.
Add the following lines to bindViews()
:
with(binding) {
// 1
txtToDoName.text = toDo.toDoName
chkToDoCompleted.isChecked = toDo.isCompleted
// 2
imgDelete.setOnClickListener(this@ViewHolder)
imgEdit.setOnClickListener(this@ViewHolder)
// 3
chkToDoCompleted.setOnCheckedChangeListener { compoundButton, _ ->
toDo.isCompleted = compoundButton.isChecked
val values = ContentValues().apply {
put(KEY_TODO_IS_COMPLETED, toDo.isCompleted)
put(KEY_TODO_ID, toDo.toDoId)
put(KEY_TODO_NAME, toDo.toDoName)
}
selectionArgs = arrayOf(toDo.toDoId.toString())
context.contentResolver.update(
Uri.parse(queryUri),
values,
selectionClause,
selectionArgs
)
}
}
In this code you:
- Fill up the
RecyclerView
item with thetoDo
item data — you provide its name and the flag that tells if the item is completed or not. - Set
setOnClickListener
for bothimgDelete
andimgEdit
. - Apply
setOnCheckedChangeListener
forchkToDoCompleted
in which you change the completed state for thetoDo
item and updatecontentResolver
with appropriate ContentValues, arguments and URI.
Then, add this block to onClick()
:
val cursor = context.contentResolver.query(
Uri.parse(queryUri),
projection,
selectionClause,
selectionArgs,
sortOrder
)
if (cursor != null) {
if (cursor.moveToPosition(bindingAdapterPosition)) {
val toDoId = cursor.getLong(cursor.getColumnIndex(KEY_TODO_ID))
val toDoName = cursor.getString(cursor.getColumnIndex(KEY_TODO_NAME))
val toDoCompleted = cursor.getInt(cursor.getColumnIndex(KEY_TODO_IS_COMPLETED)) > 0
cursor.close()
val toDo = ToDo(toDoId, toDoName, toDoCompleted)
when (imgButton?.id) {
binding.imgDelete.id -> {
deleteToDo(toDo.toDoId)
}
binding.imgEdit.id -> {
editToDo(toDo)
}
}
}
}
With this code you set an action that will happen once the user click ons the delete or update button — deleteToDo()
and editToDo()
. Both of these methods are currently empty, you’ll implement them in a bit. To know which item to update or delete, you use query()
that returns a cursor
containing. quieried to-do item’s data . You use that data to create a ToDo
item so you can pass it to deleteToDo()
and its id to editToDo()
as parameters.
Next, you’ll utilize the content provider by calling the content resolver’s functions.
The adapter needs to know how many rows there are to properly configure RecyclerView
. Locate getItemCount()
and insert this code above return
:
// Get the number of records from the Content Resolver
val cursor = context.contentResolver.query(
Uri.parse(queryCountUri),
projection, selectionClause,
selectionArgs, sortOrder
)
// Return the count of records
if (cursor != null) {
if (cursor.moveToFirst()) {
val itemCount = cursor.getInt(0)
cursor.close()
return itemCount
}
}
The query above utilizes the query String
queryCountURI, which is appended with /count to query the provider for the count of records instead of returning all the records or a single item. Now wouldn’t it be neat if another app could utilize these queries as well?
Now, find TODO
in onBindViewHolder()
and replace it wiith this:
// 1
val cursor = context.contentResolver.query(
Uri.parse(queryUri),
projection,
selectionClause,
selectionArgs, sortOrder
)
// 2
if (cursor != null) {
if (cursor.moveToPosition(position)) {
val toDoId = cursor.getLong(cursor.getColumnIndex(KEY_TODO_ID))
val toDoName = cursor.getString(cursor.getColumnIndex(KEY_TODO_NAME))
val toDoCompleted = cursor.getInt(cursor.getColumnIndex(KEY_TODO_IS_COMPLETED)) > 0
cursor.close()
val toDo = ToDo(toDoId, toDoName, toDoCompleted)
holder.bindViews(toDo)
}
}
In this code you:
- Call
query()
on the content resolver returnscursor
that allows the app to iterate through all the records in the table, create a to-do item and bind the fields of theRecyclerView
’s row to the fields of that to-do item. - Create a
ToDo
object from the fields in the query and bind it to the view.
Lastly, add the code to insert a record. Then, you can test the basic functionality of the content provider.
Inserting items
Open ToDoContentProvider.kt and locate insert()
. Replace its TODO
in with:
values?.let {
val id = db.insert(it.getAsString(KEY_TODO_NAME))
return Uri.parse("$CONTENT_URI/$id")
}
return null
When inserting a to-do item, the only relevant information that is provided is the name of that item. Completed is false by default and the id field is provided by insert()
itself. The database handler returns the id, and the content provider uses this id to return a URI where this provider can access the new record.
Next, open ToDoAdapter.kt and find insertToDo()
. Replace TODO
with:
// 1
val values = ContentValues()
values.put(KEY_TODO_NAME, toDoName)
// 2
context.contentResolver.insert(CONTENT_URI, values)
notifyDataSetChanged()
Here you:
- Create and populate a content values object.
- Run the insert query on the content resolver to insert the record. Then, notify adapter that change happened.
After all your hard work, it’s time to run the app and add a couple items! Build and run the app. Click the button in the lower left corner and add some items to your TODO list. They will be displayed in the list as you add them. Nice work! :]
The update and delete buttons will not actually do anything, yet. You’ll add the functionality to update (edit) an item, next.
Updating items
Open ToDoContentProvider.kt and copy the code below into the body of update()
:
values?.let {
val toDo = ToDo(
it.getAsLong(KEY_TODO_ID),
it.getAsString(KEY_TODO_NAME),
it.getAsBoolean(KEY_TODO_IS_COMPLETED)
)
return db.update(toDo)
}
return 0
First, you create a ToDo
item by extracting the values utilizing the key values defined in the contract. Then pass this toDo
to db to update the database.
Now, open ToDoAdapter.kt and replace TODO
in editToDo()
with the following:
// 1
val values = ContentValues().apply {
put(KEY_TODO_NAME, dialogToDoItemBinding.edtToDoName.text.toString())
put(KEY_TODO_ID, toDo.toDoId)
put(KEY_TODO_IS_COMPLETED, toDo.isCompleted)
}
// 2
context.contentResolver.update(
Uri.parse(queryUri),
values,
selectionClause,
selectionArgs
)
// 3
notifyDataSetChanged()
In this code block you:
- Create and populate the
ContentValues
object. - Run the update query to update the record.
- Notify the adapter that the dataset has changed so
RecyclerView
updates.
Run the app and use the pencil icon to update an item that you added previously.
The app can now update items. But what about deleting them? You will add the ability to delete items in the next steps.
Deleting items
Deleting a record is simple. Open ToDoContentProvider.kt and replace TODO
of delete()
:
selectionArgs?.get(0)?.let {
return db.delete(parseLong(it))
}
return 0
This gets the id for the record to delete out of the selectionArgs
and pass that value to the database handler to delete the record from the underlying database. The number of rows that are deleted is returned.
Next, open ToDoAdapter.kt and replace TODO
of deleteToDo()
with:
// 1
selectionArgs = arrayOf(id.toString())
// 2
context.contentResolver.delete(Uri.parse(queryUri), selectionClause, selectionArgs)
// 3
notifyDataSetChanged()
Toast.makeText(context, "Item deleted.", LENGTH_LONG).show()
Here you:
- Populate selectionArgs with the id of the record to delete.
- Run the query to delete the record.
- Notify the adapter that the dataset has changed and show a
Toast
message.
Run the app, you are now able to delete items from the list. Great!
Now you have created your very own content provider and content resolver in one. Even though it’s not required to utilize a content provider when outside apps aren’t accessing the data, sometimes it can provide an organizational layer of abstraction that can improve an app’s overall architecture. Now, if you’d like, you can tackle an additional challenge and create a client app that will utilize the content provider.
Challenge: Creating a client
For an additional, interesting challenge, make a copy of the app you just created that creates a content provider and see if you can remove the database and transform it into a client that utilizes the content provider.
Challenge Solution: Creating a client
It’s easy to create a client app that utilizes the provider you just created in the previous steps. You can achieve this by making a copy of the provider app and deleting the database and content provider from it. This proves the content provider is shared with an external app. The steps are as follows:
- Locate your final contentprovider app in the file system and make a copy of the project folder with name final_client.. Append the word client to the name of the folder so you can tell the difference between the two apps.
- Open the app in Android Studio by going to File > Open… and select the new folder and click OK.
- Rename the package in the project to com.raywenderlich.android.contentprovidertodoclient by right clicking on the package and selecting Refactor > Rename…. A dialog emerges asking if you’d like to rename the directory or the package. Click Rename Package. You are then prompted for the new name so type that in and click Refactor. The last step is to click the Do Refactor button at the bottom of the editor.
- Open the build.gradle app module and change the app id to com.raywenderlich.android.contentprovidertodoclient.
- Open strings.xml and change the name element value to Content Provider To Do List Client.
- Open AndroidManifest.xml and add the following permission just inside the
<manifest>
tag.
<uses-permission android:name = "com.raywenderlich.android.contentprovidertodo.provider.PERMISSION"/>
This allows the client to use whatever permissions the provider set.
- Delete the
<provider>
tag and its contents. This app does not provide data, it simply relies on the data shared by the other app. - Remove the files model/ToDoDbSchema.kt, controller/provider/ToDoContentProvider.kt and controller/ToDoDatabaseHandler.kt by right clicking them, selecting Refactor > Safe Delete…. Uncheck any boxes that would search for usages and hit OK. If the Usages Detected dialog pops up simply press Delete Anyway. Allow the editor to sync the gradle dependencies if prompted to.
- Once those files are removed, run the app. Any items you’ve added in the previous app will show in the new app. But how is this possible? You just deleted the database handler class as well as the schema and the content provider. If you create, update or delete any to-do items and then run the previous app that contains the content provider, those changes will reflect in that app as well. The two apps are sharing data through the same content provider.
Congratulate yourself! You just created two apps that are sharing the same data, think of the possibilities!
Key points
- Content providers sit just above the data source of the app, providing an additional level of abstraction from the data repository.
- A content provider allows for the sharing of data between apps.
- A content provider can be a useful way of making a single data provider if the data is housed in multiple repositories for the app.
- It’s not necessary to use a content provider if the app is not intended to share data with other apps.
- The content resolver is utilized to run queries on the content provider.
- Using a content provider can allow granular permissions to be set on the data.
- Use best practices such as selection clauses and selection arguments to prevent SQL Injection when utilizing a content provider, or any raw query data mechanism.
- Data in a content provider can be accessed via exposed URIs that are matched by the content provider.
Where to go from here?
See Google’s documentation on content provider here: https://developer.android.com/guide/topics/providers/content-providers.
Learn more specifics about content provider permissions here: https://developer.android.com/guide/topics/providers/content-provider-basics#Permissions.
Learn more about how to protect against malicious data from this guide found here: https://developer.android.com/guide/topics/providers/content-provider-basics#Injection
Find out more about using content providers with Storage Access Framework here: https://developer.android.com/guide/topics/providers/document-provider.