文章目录
- 简介
- 一、准备数据:创建 SQLite 数据库
- 二、创建 ContentProvider
- 三、在其他应用程序中操作此 ContentProvider
- 四、借助 ContentProvider 访问系统通讯录
简介
ContentProvider 用于应用程序间数据共享。比如系统的通讯录,短信、媒体库中的数据,都对外提供了 ContentProvider,使得我们可以很方便的访问其中的数据。当然,我们也可以自定义 ContentProvider 为其他程序提供数据,实现程序间的数据共享。ContentProvider 使用起来和数据库非常类似,常用的方法就是增删改查。
接下来我们先创建一个数据库,再使用 ContentProvider 将其共享出去。
一、准备数据:创建 SQLite 数据库
由于操作 SQLite 数据库不是本文的重点,所以我们快速的过一遍。新建一个应用程序,包名是 com.example.contentproviderdemo
。
新建 MyDbHelper 类:
class MyDbHelper(context: Context, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version) {
private val createBook = """create table Book (
id integer primary key autoincrement,
name text,
price real)
""".trimMargin()
private val createCategory = """create table Category (
id integer primary key autoincrement,
name text,
code integer)
""".trimMargin()
override fun onCreate(db: SQLiteDatabase?) {
db?.execSQL(createBook)
db?.execSQL(createCategory)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
}
在这个类中,我们创建了两个表,Book 和 Category,每个表都有一个 id 和两个字段。
接下来在 MainActivity 中,创建这两个表并插入几行数据:
class MainActivity : AppCompatActivity() {
private val db by lazy { MyDbHelper(this, "BookStore.db", 1).writableDatabase }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnCreate.setOnClickListener {
db.apply {
insert("Book", null, contentValuesOf("name" to "第一行代码", "price" to 99.00))
insert("Book", null, contentValuesOf("name" to "Android 源码设计模式解析与实战", "price" to 99.00))
insert("Category", null, contentValuesOf("name" to "Android", "code" to 1))
insert("Category", null, contentValuesOf("name" to "Android", "code" to 2))
}
}
}
}
布局文件中只有一个 id 为 btnCreate 的按钮,故不再给出布局代码。运行程序,点击一次按钮,两个表就会被创建,并分别插入两条数据,这样我们数据的准备工作就完成了。
可以用 DataBase Navigator
插件查看这个数据库文件,文件所在的路径是 /data/data/包名/databases
,文件名称是 BookStore.db
。
Book 表:
id | name | price |
1 | 第一行代码 | 99 |
2 | Android 源码设计模式解析与实战 | 99 |
Category 表:
id | name | price |
1 | Android | 1 |
2 | Android | 2 |
Ok,数据准备好之后,我们就可以开始编写 ContentProvider 了。
二、创建 ContentProvider
新建 MyContentProvider 类,继承自 ContentProvider,并实现其中的抽象方法:
class MyContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
}
override fun getType(uri: Uri): String? {
}
}
一共有六个抽象方法需要实现。
- onCreate 会在 ContentProvider 初始化时调用
- insert、delete、update、query 分别对应共享数据的增删改查
- getType 用于获取 Uri 对象所对应的 MIME 类型,暂时不理解 MIME 类型也没关系,不妨把它记做固定写法,它由三部分构成
- 必须以
vnd
开头 - 如果内容 URI 以路径结尾,则后接
android.cursor.dir/
;如果以 id 结尾,则后接android.cursor.item/
- 最后接上
vnd.<authority>.<path>
看一下 MyContentProvider 的最终实现:
const val BookStore = "BookStore.db"
const val Book = "Book"
const val Category = "Category"
const val bookDir = 0
const val bookItem = 1
const val categoryDir = 2
const val categoryItem = 3
const val authority = "com.example.contentproviderdemo.provider"
class MyContentProvider : ContentProvider() {
private lateinit var db: SQLiteDatabase
private val uriMatcher by lazy {
UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(authority, "book", bookDir)
addURI(authority, "book/#", bookItem)
addURI(authority, "category", categoryDir)
addURI(authority, "category/#", categoryItem)
}
}
override fun onCreate() = context?.let {
db = MyDbHelper(it, BookStore, 1).writableDatabase
true
} ?: throw NullPointerException()
override fun insert(uri: Uri, values: ContentValues?) = when (uriMatcher.match(uri)) {
bookDir, bookItem -> {
val newBookId = db.insert(Book, null, values)
Uri.parse("content://$authority/book/$newBookId")
}
categoryDir, categoryItem -> {
val newCategoryId = db.insert(Category, null, values)
Uri.parse("content://$authority/category/$newCategoryId")
}
else -> null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = when (uriMatcher.match(uri)) {
bookDir -> db.delete(Book, selection, selectionArgs)
bookItem -> db.delete(Book, "id = ?", arrayOf(uri.pathSegments[1]))
categoryDir -> db.delete(Category, selection, selectionArgs)
categoryItem -> db.delete(Category, "id = ?", arrayOf(uri.pathSegments[1]))
else -> 0
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
) = when (uriMatcher.match(uri)) {
bookDir -> db.update(Book, values, selection, selectionArgs)
bookItem -> db.update(Book, values, "id = ?", arrayOf(uri.pathSegments[1]))
categoryDir -> db.update(Category, values, selection, selectionArgs)
categoryItem -> db.update(Category, values, "id = ?", arrayOf(uri.pathSegments[1]))
else -> 0
}
@SuppressLint("Recycle")
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
) = when (uriMatcher.match(uri)) {
bookDir -> db.query(Book, projection, selection, selectionArgs, null, null, sortOrder)
bookItem -> db.query(Book, projection, "id = ?", arrayOf(uri.pathSegments[1]), null, null, sortOrder)
categoryDir -> db.query(Category, projection, selection, selectionArgs, null, null, sortOrder)
categoryItem -> db.query(Category, projection, "id = ?", arrayOf(uri.pathSegments[1]), null, null, sortOrder)
else -> null
}
override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
bookDir -> "vnd.android.cursor.dir/vnd.$authority.book"
bookItem -> "vnd.android.cursor.item/vnd.$authority.book"
categoryDir -> "vnd.android.cursor.dir/vnd.$authority.category"
categoryItem -> "vnd.android.cursor.item/vnd.$authority.category"
else -> null
}
}
代码较长,但并不复杂。
- 在 onCreate 方法中,初始化 SQLiteDatabase 变量 db,用于待会的数据库操作
- insert 方法中,根据 Uri 插入不同的表格,然后将插入数据的内容 Uri 返回
- delete 方法中,根据 Uri 删除不同的表格中的数据,然后将删除的数据条数返回
- update 方法中,根据 Uri 更新不同的表格中的数据,然后将更新的数据条数返回
- query 方法中,根据 Uri 查询不同的表格中的数据,将查询出的 Cursor 对象返回
- getType 方法中,根据上文所说的规则拼接出字符串返回即可
这几个方法都用到了 Uri,那么 Uri 是什么呢?
其实 Uri 就相当于一个地址,它主要由三部分组成:前缀、authority 和 path。
- 前缀
content://
是固定格式,用来表示这是一个内容 Uri。 - authority 一般是程序的包名加上
.provider
,用于指定是哪个应用程序中的 ContentProvider,对应本例中的com.example.contentproviderdemo.provider
- path 指路径,用于区分同一个程序中不同的数据表,对应本例中的
/book
和/category
,path 后面还可以后缀一个 id,比如/book/1
表示 Book 表中 id 为 1 的元素
由此可知,从 Uri 中我们就可以知道需要操作的是哪个应用程序中的哪个表格,甚至精确到哪条数据。这是一个将地址封装起来的思想,只不过它没有用单独的类封装,而是封装在一个字符串中,方便我们使用。
Uri 可以很方便的取出 path
中的每一个元素,uri.pathSegments 就是用来做这个的,它会将 path
中的每一部分分割出来,保存到列表中。如:
-
content://com.example.contentproviderdemo.provider/book
分割后, uri.pathSegments 中存储的就是[book]
-
content://com.example.contentproviderdemo.provider/book/1
分割后, uri.pathSegments 中存储的就是[book, 1]
所以当需要操作的是 item 时,我们使用了 uri.pathSegments[1] 表示 id。
这里还用到了一个 UriMatcher 类,它是用来辅助我们匹配 URI 的,UriMatcher 类似于一个 HashMap<Uri, code>
。
- 先通过
addURI(authority: String?, path: String?, code: Int)
方法往 UriMatcher 中添加了许多 URI - 然后再用
uriMatcher.match(uri)
方法来匹配传入的 Uri,如果 addURI 时传入的前两个参数这样拼接出来的 URIcontent://$authority/$path
和 match 方法传入的 uri 一致,就会返回 addURI 方法中传入的第三个参数 code - 如果 UriMatcher 中没有任何一个 URI 能和传入的 Uri 匹配上,则返回构造方法中传入的默认参数
UriMatcher.NO_MATCH
ContentProvider 写好后,需要在 AndroidManifest 中注册:
<application
...>
...
<provider
android:name=".MyContentProvider"
android:authorities="com.example.contentproviderdemo.provider"
android:enabled="true"
android:exported="true" />
</application>
- exported 表示是否对外分享此 ContentProvider,默认是 false,我们需要对外分享,所以将其设置成 true
- enabled 表示是否启用此 ContentProvider,默认就是 true,不过为了防止 Android 在以后的版本更新中修改默认值,我们最好把两个属性都设置好。
三、在其他应用程序中操作此 ContentProvider
另外新建一个应用程序,编辑 MainActivity:
class MainActivity : AppCompatActivity() {
private val bookUri by lazy { Uri.parse("content://com.example.contentproviderdemo.provider/book") }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnAdd.setOnClickListener {
contentResolver.insert(bookUri, contentValuesOf("name" to "大话设计模式", "price" to 45.00))
}
btnDelete.setOnClickListener {
contentResolver.delete(bookUri, "name = ?", arrayOf("大话设计模式"))
}
btnUpdate.setOnClickListener {
contentResolver.update(bookUri, contentValuesOf("price" to 99.00), "name = ?", arrayOf("大话设计模式"))
}
btnQuery.setOnClickListener {
val cursor = contentResolver.query(bookUri, null, null, null, null)
cursor?.apply {
while (moveToNext()) {
val name = getString(getColumnIndex("name"))
val price = getDouble(getColumnIndex("price"))
Log.d("~~~", "$name: $price")
}
close()
}
}
}
}
布局文件中只有四个 id 是 btnAdd、btnDelete、btnUpdate、btnQuery 的按钮,故不再给出布局代码。
访问 ContentProvider 中的数据需要借助 ContentResolver 类,调用这个类的 insert、delete、update、query 方法时,就会回调我们刚才写的 ContentProvider 中的对应方法。
点击 btnQuery,Log 如下:
~~~: 第一行代码: 99.0
~~~: Android 源码设计模式解析与实战: 99.0
这就是我们刚才的应用程序中创建的 Book 表中的数据。
点击 btnAdd 后,再点击 btnQuery,Log 如下:
~~~: 第一行代码: 99.0
~~~: Android 源码设计模式解析与实战: 99.0
~~~: 大话设计模式: 45.0
说明我们添加数据成功了,再测试一下更新数据和删除数据。
点击 btnUpdate 后,再点击 btnQuery,Log 如下:
~~~: 第一行代码: 99.0
~~~: Android 源码设计模式解析与实战: 99.0
~~~: 大话设计模式: 99.0
点击 btnDelete 后,再点击 btnQuery,Log 如下:
~~~: 第一行代码: 99.0
~~~: Android 源码设计模式解析与实战: 99.0
这就说明我们对上一个程序共享的 ContentProvider 数据的增删改查操作都成功了。
四、借助 ContentProvider 访问系统通讯录
前文说到,系统的通讯录也为我们提供了 ContentProvider,那么我们就来尝试查询一下系统通讯录的数据吧。
先打开通讯录,添加一条数据:
在 AndroidManifest 中申请读取通讯录权限:
<uses-permission android:name="android.permission.READ_CONTACTS" />
MainActivity 中添加如下代码:
import android.provider.ContactsContract.CommonDataKinds.Phone
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 1)
val cursor = contentResolver.query(Phone.CONTENT_URI, null, null, null, null)
cursor?.apply {
while (moveToNext()) {
val displayName = getString(getColumnIndex(Phone.DISPLAY_NAME))
val number = getString(getColumnIndex(Phone.NUMBER))
Log.d("~~~", "$displayName: $number")
}
close()
}
}
}
由于 Android 6.0 以后,READ_CONTACTS
被划分为危险权限,所以我们需要在程序运行时调用 requestPermissions
方法动态申请这个权限。由于动态申请权限不是本文的重点,所以笔者只是简单的申请了一下,没有处理用户拒绝权限后的操作。
运行程序,同意权限后,输出如下:
~~~: Alpinist Wang: (666) 666-666
说明我们访问通讯录数据成功了!
顺便说一下查询时不传入 null 值的写法,一个携带所有参数的 query 语句如下,和 SQLite 查询一模一样。事实上,这里的参数传到 ContentProvider 后,就是调用的 SQLite 的查询:
val cursor = contentResolver.query(Phone.CONTENT_URI, arrayOf(Phone.DISPLAY_NAME, Phone.NUMBER), "${Phone.DISPLAY_NAME} = ?", arrayOf("Alpinist Wang"), Phone.DISPLAY_NAME)
cursor?.apply {
while (moveToNext()) {
val displayName = getString(getColumnIndex(Phone.DISPLAY_NAME))
val number = getString(getColumnIndex(Phone.NUMBER))
Log.d("~~~", "$displayName: $number")
}
close()
}
意思是筛选出 Phone.DISPLAY_NAME
的值为 “Alpinist Wang” 的所有行,取出这些数据中的 Phone.DISPLAY_NAME
、Phone.NUMBER
这两列,最后按照 Phone.DISPLAY_NAME
排序。运行程序,输出和刚才一样。
这就是通过 ContentProvider 读取系统通讯录的方法,不过要想对系统通讯录进行增删改,和我们自定义的 ContentProvider 有点出入的,因为通讯录涉及多个表,所以必须同时修改多个表才行,感兴趣的读者可以自行查阅文档了解。
以上就是 ContentProvider 的使用方式,至此,我们已将 Android 四大组件都梳理了一遍,对其他组件感兴趣的读者可以访问本专栏的其他文章查看。