Android 四大组件(四) —— 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 表:

idnameprice
1第一行代码99
2Android 源码设计模式解析与实战99

Category 表:

idnameprice
1Android1
2Android2

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 时传入的前两个参数这样拼接出来的 URI content://$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_NAMEPhone.NUMBER 这两列,最后按照 Phone.DISPLAY_NAME 排序。运行程序,输出和刚才一样。

这就是通过 ContentProvider 读取系统通讯录的方法,不过要想对系统通讯录进行增删改,和我们自定义的 ContentProvider 有点出入的,因为通讯录涉及多个表,所以必须同时修改多个表才行,感兴趣的读者可以自行查阅文档了解。

以上就是 ContentProvider 的使用方式,至此,我们已将 Android 四大组件都梳理了一遍,对其他组件感兴趣的读者可以访问本专栏的其他文章查看。

参考文章

《第一行代码》(第三版)- 第 8 章 跨程序共享数据,探究 ContentProvider

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值