Content providers can help an application manage access to data stored by itself, stored by other apps, and provide a way to share data with other apps.
一、定义
Google的文档定义非常清晰,Content Provider就是用来与其他进程共享数据的标准方式。例如,社交应用需要获取你的联系人数据,为你迅速找到朋友的账号,联系人数据就是从系统应用联系人提供的Content Provider中获取的。它的具体结构可以用下图来展示。
具体来说,数据存储在Sqlite或者其他持久化方案中,Content Provider对外暴露对这些持久化数据的CRUD接口,而其他应用则可以通过这些接口完成对数据的读写。
二、为应用创建Content Provider
下面就基于SQLite3数据库,创建一个可以为其他应用提供CRUD接口的简单应用,一步步地学习Content Provider
。该应用将为外界提供一组NBA球员的数据,其他应用可以通过接口对这些数据进行读写。
2.1 创建Content Provider
使用Android Studio
(我使用的是最新的4.1.2版本)可以轻易创建一个Content Provider
,先创建一个应用,因为是单纯的数据提供应用,所以选择No Activity的就行。在包名目录下右键new->other->Content Provider,如下图所示。
在新的窗口中为Content Provider
命名,这里我使用了NBAPlayersContentProvider
,URI Authority这一栏用于填写对外暴露的URI接口,其他应用将使用这个值作为URI的host部分(暂时认为这是NBAPlayersContentProvider
的ID吧),一般的惯用写法是:
Copy <package_name>.<provider_name>
这里我们由于只有一个provider
,所以命名为com.rodneycheung.contentprovidersample.provider
。Exported
和Enabled
都勾上,不勾也没事,反正在AndroidManifest.xml
中也可以加上,然后点击Finish
即可。
创建完成后,Android Studio
会为我们自动生成代码如下:
Copy class NBAPlayersContentProvider : ContentProvider () {
override fun delete (uri: Uri , selection: String ?, selectionArgs: Array < String >?): Int {
TODO ( "Implement this to handle requests to delete one or more rows" )
}
override fun getType (uri: Uri ): String ? {
TODO (
"Implement this to handle requests for the MIME type of the data" +
"at the given URI"
)
}
override fun insert (uri: Uri , values: ContentValues ?): Uri ? {
TODO ( "Implement this to handle requests to insert a new row." )
}
override fun onCreate (): Boolean {
TODO ( "Implement this to initialize your content provider on startup." )
}
override fun query (
uri: Uri , projection: Array < String >?, selection: String ?,
selectionArgs: Array < String >?, sortOrder: String ?
): Cursor ? {
TODO ( "Implement this to handle query requests from clients." )
}
override fun update (
uri: Uri , values: ContentValues ?, selection: String ?,
selectionArgs: Array < String >?
): Int {
TODO ( "Implement this to handle requests to update one or more rows." )
}
}
2.2 Content Provider接口解释
Android Studio
为我们自动生成的NBAPlayersContentProvider
继承于ContentProvider
,对于每个重写的函数都标注了函数的作用。我们一共需要实现一下几个,首先是onCreate
方法,这个用于Content Provider创建的时候做的一些初始化工作;然后是getType
,这个用于返回每个CRUD接口的MIME类型;最后就是对外暴露的CRUD接口,跟数据库中的很像,连参数都类似。query
用于从查询数据,insert
用于插入数据,update
用于更新数据,delete
用于删除数据。其实这些对于有数据库开发基础的开发者都很好理解。
2.3 实现数据持久化部分
前面的图解中说了,Content Provider
中的数据来源于持久化方案中,所以我们先基于SQLite3把数据部分实现了。我们只需要创建一张表Players
,表结构如下:
下面创建数据库类NbaDbHelper
Copy class NbaDbHelper ( private val context: Context , name: String , version: Int ) :
SQLiteOpenHelper (context, name, null , version){
override fun onCreate (db: SQLiteDatabase ?) {
TODO ( "Not yet implemented" )
}
override fun onUpgrade (db: SQLiteDatabase ?, oldVersion: Int , newVersion: Int ) {
TODO ( "Not yet implemented" )
}
}
首先在onCreate
方法中实现数据库的创建和升级逻辑:
Copy class NbaDbHelper ( private val context: Context , name: String , version: Int ) :
SQLiteOpenHelper (context, name, null , version) {
private val TABLE_PLAYER = "player"
private val COLUMN_NAME = "name"
private val COLUMN_SCORE = "score"
private val COLUMN_REBOUND = "rebound"
private val COLUMN_ASSISTS = "assists"
private val COLUMN_BLOCK = "block"
private val COLUMN_STEALS = "steals"
override fun onCreate (db: SQLiteDatabase ?) {
db?. execSQL ( "create table $TABLE_PLAYER ($COLUMN_NAME text primary key, $COLUMN_SCORE real,$COLUMN_REBOUND real,$COLUMN_ASSISTS real,$COLUMN_BLOCK real,$COLUMN_STEALS real)" )
}
override fun onUpgrade (db: SQLiteDatabase ?, oldVersion: Int , newVersion: Int ) {
db?. execSQL ( "drop table if exists $TABLE_PLAYER" )
onCreate (db)
}
}
然后我们添加一些供NBAPlayersContentProvider
访问的数据读写接口。
Copy class NbaDbHelper ( private val context: Context , name: String , version: Int ) :
SQLiteOpenHelper (context, name, null , version) {
private val TABLE_PLAYER = "player"
private val COLUMN_NAME = "name"
private val COLUMN_SCORE = "score"
private val COLUMN_REBOUND = "rebound"
private val COLUMN_ASSISTS = "assists"
private val COLUMN_BLOCK = "block"
private val COLUMN_STEALS = "steals"
override fun onCreate (db: SQLiteDatabase ?) {
db?. execSQL ( "create table $TABLE_PLAYER ($COLUMN_NAME text primary key, $COLUMN_SCORE real,$COLUMN_REBOUND real,$COLUMN_ASSISTS real,$COLUMN_BLOCK real,$COLUMN_STEALS real)" )
}
override fun onUpgrade (db: SQLiteDatabase ?, oldVersion: Int , newVersion: Int ) {
db?. execSQL ( "drop table if exists $TABLE_PLAYER" )
onCreate (db)
}
fun insertPlayer (values: ContentValues ?): Long {
return writableDatabase. insert (TABLE_PLAYER, null , values)
}
fun updatePlayer (
values: ContentValues ?, selection: String ?,
selectionArgs: Array < String >?
): Int {
return writableDatabase. update (TABLE_PLAYER, values, selection, selectionArgs)
}
fun deletePlayer (selection: String ?, selectionArgs: Array < String >?): Int {
return writableDatabase. delete (TABLE_PLAYER, selection, selectionArgs)
}
fun queryPlayer (
projection: Array < String >?, selection: String ?,
selectionArgs: Array < String >?, sortOrder: String ?
): Cursor {
return readableDatabase. query (
TABLE_PLAYER,
projection,
selection,
selectionArgs,
null ,
null ,
sortOrder
)
}
}
至此,数据库部分就是先完成了。
2.4 完成Content Provider
2.4.1 onCreate
该函数在Content Provider
创建时调用,一般我们会在这里初始化数据库。
Copy class NBAPlayersContentProvider : ContentProvider () {
private lateinit var dbHelper: NbaDbHelper
override fun onCreate (): Boolean = context?. let {
dbHelper = NbaDbHelper (it, "nba.db" , 1 )
true
} ?: false
.. .
}
2.4.2 getType
该函数用于返回内容Uri的MIME类型。先解释一下什么叫内容Uri,之前的NBAPlayersContentProvider
方法中很多都带一个叫Uri
的参数,这个就是所谓的内容Uri。一个标准的内容Uri的格式是:
Copy content://<authority>/<path>
authority
就是前面创建NBAPlayersContentProvider
时的值,path
很好理解,用于定位NBAPlayersContentProvider
中的资源。一般path
的格式为:
Copy tableName 在tableName表中进行读写
tableName/* 在tableName表中进行被命名为*的操作
tableName/# 读取tableName表中ID为#的行
*
表示任意字符串,#
表示任意数字。在这里,我们稍微简单一点,只用第一种格式,即可以在整张表中通过SQL约束来进行增删改查。对于tableName/*
,一般用于对外暴露一个封装好的接口,即用户不需要了解数据存储的表结构,也可以获取到相应的数据。例如player/getAllTripleDouble
,就是获取本赛季所有数据达到场均三双的球员。用户只需要使用contentResolver.query("com.rodneycheung.contentprovidersample.provider/player/getAllTripleDouble",...)
就可以获取到所有场均三双球员了。
在了解了内容Uri的定义后,再介绍一个在ContentProvider中常用的Uri匹配的类UriMatcher
,它可以帮助我们匹配各个方法中的参数Uri
的值。下面直接上做法,非常简单易懂,就不多说了。
Copy class NBAPlayersContentProvider : ContentProvider () {
private lateinit var dbHelper: NbaDbHelper
private val playerTable = 0
private val authority = "com.rodneycheung.contentprovidersample.provider"
private val uriMatcher by lazy {
val matcher = UriMatcher (UriMatcher.NO_MATCH)
matcher. addURI (authority, "player" , playerTable)
matcher
}
override fun onCreate (): Boolean = context?. let {
dbHelper = NbaDbHelper (it, "nba.db" , 1 )
true
} ?: false
}
在定义好UriMatcher
后,我们就不需要手动去解析uri
参数并和我们预设的路径进行匹配了。
Android
对于这里的MIME
类型作了三点格式的规定:
如果内容Uri是以路径结尾,vnd
后面接android.cursor.dir/
,否则接android.cursor.item
最后接上vnd.<authority>.<path>
由于我们只定义了一个针对整个player
表的路径,那么我们的MIME
字符串就是vnd.android.cursor.dir/vnd.com.rodneycheung.contentprovidersample.provider.player
,下面来实现getType
方法。
Copy class NBAPlayersContentProvider : ContentProvider () {
private lateinit var dbHelper: NbaDbHelper
private val playerTable = 0
private val authority = "com.rodneycheung.contentprovidersample.provider"
private val uriMatcher by lazy {
val matcher = UriMatcher (UriMatcher.NO_MATCH)
matcher. addURI (authority, "player" , playerTable)
matcher
}
override fun onCreate (): Boolean = context?. let {
dbHelper = NbaDbHelper (it, "nba.db" , 1 )
true
} ?: false
override fun getType (uri: Uri ): String ? {
return when (uriMatcher. match (uri)) {
playerTable -> "vnd.android.cursor.dir/vnd.com.rodneycheung.contentprovidersample.provider.player"
else -> null
}
}
}
2.4.3 CRUD接口
接下来就是实现CRUD接口,由于我们直接对于player
表进行操作,所以非常简单,直接把参数填到dbHelper
对应的接口里就可以了。
Copy class NBAPlayersContentProvider : ContentProvider () {
private lateinit var dbHelper: NbaDbHelper
private val playerTable = 0
private val authority = "com.rodneycheung.contentprovidersample.provider"
private val uriMatcher by lazy {
val matcher = UriMatcher (UriMatcher.NO_MATCH)
matcher. addURI (authority, "player" , playerTable)
matcher
}
override fun onCreate (): Boolean = context?. let {
dbHelper = NbaDbHelper (it, "nba.db" , 1 )
true
} ?: false
override fun getType (uri: Uri ): String ? {
return when (uriMatcher. match (uri)) {
playerTable -> "vnd.android.cursor.dir/vnd.com.rodneycheung.contentprovidersample.provider.player"
else -> null
}
}
override fun delete (uri: Uri , selection: String ?, selectionArgs: Array < String >?): Int {
return when (uriMatcher. match (uri)) {
playerTable -> dbHelper. deletePlayer (selection, selectionArgs)
else -> 0
}
}
override fun insert (uri: Uri , values: ContentValues ?): Uri ? {
return when (uriMatcher. match (uri)) {
playerTable -> {
Uri. parse ( "content://$authority/player/ ${ dbHelper. insertPlayer (values) } " )
}
else -> null
}
}
override fun query (
uri: Uri , projection: Array < String >?, selection: String ?,
selectionArgs: Array < String >?, sortOrder: String ?
): Cursor ? {
return when (uriMatcher. match (uri)) {
playerTable -> dbHelper. queryPlayer (projection, selection, selectionArgs, sortOrder)
else -> null
}
}
override fun update (
uri: Uri , values: ContentValues ?, selection: String ?,
selectionArgs: Array < String >?
): Int {
return when (uriMatcher. match (uri)) {
playerTable -> dbHelper. updatePlayer (values, selection, selectionArgs)
else -> 0
}
}
}
2.4.4 权限申请
在Android 11
中,对于Content Provider
的权限又收紧了,需要在AndroidManifest.xml
中进行读写权限的申请,具体做法如下。
Copy <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rodneycheung.contentprovidersample">
<!-- 声明读写两种权限-->
<permission
android:name="com.rodneycheung.contentprovidersample.provider.READ_PERMISSION"
android:label="contentprovidersample provider read pomission"
android:protectionLevel="normal" />
<permission
android:name="com.rodneycheung.contentprovidersample.provider.WRITE_PERMISSION"
android:label="contentprovidersample provider write pomission"
android:protectionLevel="normal" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ContentProviderSample">
<!-- 引用读写权限-->
<provider
android:name=".NBAPlayersContentProvider"
android:authorities="com.rodneycheung.contentprovidersample.provider"
android:enabled="true"
android:exported="true"
android:readPermission="com.rodneycheung.contentprovidersample.provider.READ_PERMISSION"
android:writePermission="com.rodneycheung.contentprovidersample.provider.WRITE_PERMISSION" />
</application>
</manifest>
此外,用户为了能找到这个provider
,还需要在自己的AndroidManifest.xml
里面加上:
Copy <queries>
<package android:name="com.rodneycheung.contentprovidersample" />
</queries>
三、总结
结束了,非常的简单😄,完整的代码在这里 。