我们知道,Android 四大组件为 Activity,Service,BroadcastReceiver 和 ContentProvider,ContentProvider 作为四大组件之一,似乎有些名不副实了,因为在一般应用开发中,使用的次数很少,但它真的是徒有虚名吗?
什么是 ContentProvider
ContentProvider 主要用于在不同的应用程序之间实现数据共享的功能,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。
什么是 URI
URI 全称是通用资源标识符,URI 给 ContentProvider 中的数据建立唯一标识符,主要由两部分组成:
- authority:用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名,比如某个应用的包名是 com.example.myapplication,那么该应用对应的 authority 就可以命名为 com.example.myapplication.provider。
- path:用于对同一应用程序中不同的表做区分的,添加到 authority 的后面。比如某个应用的数据库里存在一张表 table,这时就可以将 path 分别命名为 /table。
然后把 authority 和 path 进行组合,标准的内容 URI 如下:
content://com.example.myapplication.provider/table
内容 URI 的格式主要有两种,以路径结尾表示访问该表中所有的数据,如上所示,以 id 结尾表示访问该表中拥有相应 id 的数据,如下所示:
content://com.example.myapplication.provider/table/1
我们可以使用通配符分别匹配这两种格式的内容 URI,* 表示匹配任意长度的任意字符。# 表示匹配任意长度的数字。
一个匹配任意表的内容 URI 可以写成
content://com.example.myapplication.provider/*
一个能够匹配 table 表中任意一行数据的内容 URI 可以写成
content://com.example.myapplication.provider/table/#
由此可见,内容 URI 可以清楚地表达我们想要访问哪个程序中哪张表里的数据。但是,有内容 URI 字符串还不行,还需要把它解析成 Uri 对象才可以作为参数传入。
val uri = Uri.parse("content://com.example.myapplication.provider/table1")
什么是 ContentResolver
如果想要访问 ContentProvider 中共享的数据,就一定要借助 ContentResolver 类,ContentResolver 中提供了一系列的方法用于对数据进行增删改查的操作。
ContentResolver 的使用
这里以读取系统联系人为例,先需要声明权限,这里使用 PermissionX
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
<uses-permission android:name="android.permission.READ_CONTACTS" />
PermissionX.init(this)
.permissions(Manifest.permission.READ_CONTACTS)
.request { allGranted, _, deniedList ->
if (allGranted) {
Toast.makeText(this, "All permissions are granted", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(
this,
"These permissions are denied: $deniedList",
Toast.LENGTH_LONG
).show()
}
}
权限授予之后,我们就可以读取联系人数据了。
contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
null,
null
)?.run {
while (moveToNext()) {
val name =
getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val phoneNumber =
getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
Log.i(tag, "name: $name,phoneNumber: $phoneNumber")
}
close()
}
创建 ContentProvider
在此之前,我们需要先创建一个数据库,这里使用 Jetpack Room 来操作 SQLite。 用到的依赖如下:
plugins {
...
id 'kotlin-kapt'
}
def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "androidx.activity:activity-ktx:1.5.1"
这里创建一个名为 Project 的表
@Entity
data class Project(
@PrimaryKey var id: String,
var technology: String,
var charge: String
)
@Database(entities = [Project::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
val instance = INSTANCE
instance?.let { return it }
synchronized(this) {
val db = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"database_name"
).build()
INSTANCE = db
return db
}
}
}
}
数据库创建好之后,我们就可以创建 ContentProvider 将该数据库操作提供出去了。在此过程中,我们需要使用 UriMatcher 来匹配内容 URI。UriMatcher 中有一个 addURI 的方法,这个方法接收三个参数,可以分别把authority,path 和 code 传进去即可。
Android Studio 创建 ContentProvider 之后,便可以往里面加入我们的逻辑了,如下所示:
<provider
android:name=".MyContentProvider"
android:authorities="com.example.myapplication.provider"
android:enabled="true"
android:exported="true"/>
class MyContentProvider : ContentProvider() {
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
private val tableName = "project"
private val dirCode = 1
private val itemCode = 2
//删除数据,uri 表示要删除数据的表,selection 和 selectionArgs 约束删除哪些行,返回被删除的行数。
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) =
when (uriMatcher.match(uri)) {
dirCode -> AppDatabase.getDatabase(context!!).openHelper.writableDatabase.delete(
tableName,
selection,
selectionArgs
)
itemCode -> {
val id = uri.pathSegments[1]
AppDatabase.getDatabase(context!!).openHelper.writableDatabase.delete(
tableName,
"id = ?",
arrayOf(id)
)
}
else -> 0
}
////根据传入的内容 URI 返回相应的 MIME 类型
override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
dirCode -> "vnd.android.cursor.dir/vnd.com.example.myapplication.provider.project"
itemCode -> "vnd.android.cursor.item/vnd.com.example.myapplication.provider.project"
else -> null
}
//添加数据,uri 确定要添加的表,values 表示要添加的数据,返回一个用于表示这条新记录的 Uri。
override fun insert(uri: Uri, values: ContentValues?) = when (uriMatcher.match(uri)) {
dirCode, itemCode -> {
val rowId = AppDatabase.getDatabase(context!!).openHelper.writableDatabase.insert(
tableName,
SQLiteDatabase.CONFLICT_REPLACE,
values
)
ContentUris.withAppendedId(uri, rowId)
}
else -> null
}
/* 初始化 ContentProvider 的时候调用。返回 true 表示初始化成功,返回 false 表示失败。初始化在
Application 的 attachBaseContext 和 onCreate 之间。*/
override fun onCreate(): Boolean {
uriMatcher.addURI("com.example.myapplication.provider", tableName, dirCode)
uriMatcher.addURI("com.example.myapplication.provider", "$tableName/#", itemCode)
return true
}
/* 查询数据:参数 uri 确定查询哪张表,projection 确定查询哪些列,selection 和 selectionArgs
约束查询哪些行,sortOrder 结果排序,查询结果存在 Cursor 对象中返回。*/
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? = when (uriMatcher.match(uri)) {
dirCode -> AppDatabase.getDatabase(context!!).openHelper.readableDatabase.query(
SupportSQLiteQueryBuilder.builder(tableName)
.selection(selection, selectionArgs)
.columns(projection)
.orderBy(sortOrder)
.create()
)
itemCode -> {
val id = uri.pathSegments[1]
AppDatabase.getDatabase(context!!).openHelper.readableDatabase.query(
SupportSQLiteQueryBuilder.builder(tableName)
.selection("id = ?", arrayOf(id))
.columns(projection)
.orderBy(sortOrder)
.create()
)
}
else -> null
}
/* 更新已有数据,uri 表示要更新数据的表,values 表示要更新的数据,selection 和 selectionArgs
约束更新哪些行,返回更新的行数。*/
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
) = when (uriMatcher.match(uri)) {
dirCode -> AppDatabase.getDatabase(context!!).openHelper.writableDatabase.update(
tableName,
SQLiteDatabase.CONFLICT_REPLACE,
values,
selection,
selectionArgs
)
itemCode -> {
val id = uri.pathSegments[1]
AppDatabase.getDatabase(context!!).openHelper.writableDatabase.update(
tableName,
SQLiteDatabase.CONFLICT_REPLACE,
values,
"id = ?",
arrayOf(id)
)
}
else -> 0
}
}
此时,该应用已用于跨进程共享数据的能力了,我们再新建一个 Project 来作为调用方,需要注意的是,调用方需要在 AndroidManifest 中 application 同级的位置添加如下代码才可操作。
<queries>
<provider android:authorities="com.example.myapplication.provider" />
</queries>
调用方添加数据
val uri = Uri.parse("content://com.example.myapplication.provider/project")
val value = contentValuesOf("id" to "1", "technology" to "web", "charge" to "Taylor")
contentResolver.insert(uri, value)
调用方删除数据
val uri = Uri.parse("content://com.example.myapplication.provider/project/1")
contentResolver.delete(uri, null, null)
val uri = Uri.parse("content://com.example.myapplication.provider/project")
contentResolver.delete(uri, "id = ?", arrayOf("1"))
调用方更新数据
val uri = Uri.parse("content://com.example.myapplication.provider/project")
val value = contentValuesOf("id" to "520", "technology" to "ios", "charge" to "Justin")
contentResolver.update(uri, value, "id = ?", arrayOf("1"))
调用方查询数据
// 查询所有
val uri = Uri.parse("content://com.example.myapplication.provider/project")
contentResolver.query(uri, null, null, null, null)?.apply {
while (moveToNext()) {
val id = getString(getColumnIndex("id"))
val technology = getString(getColumnIndex("technology"))
val charge = getString(getColumnIndex("charge"))
Log.i(tag, "id: $id, technology: $technology, charge: $charge")
}
close()
}
}
//查询的列为 id 和 charge,查询条件为 id < 5,结果根据 id 排序
val uri = Uri.parse("content://com.example.myapplication.provider/project")
contentResolver.query(uri, arrayOf("id", "charge"), "id < ?", arrayOf("5"), "id")
?.apply {
while (moveToNext()) {
val id = getString(getColumnIndex("id"))
val charge = getString(getColumnIndex("charge"))
Log.i(tag, "id: $id, charge: $charge")
}
close()
}