04.ContentProvider总结

521 阅读3分钟

ContentProvider是四大组件之一,常用来实现进程间通信,提供供其他应用调用的数据接口等。直接调用ContentResolve,可以获取系统音乐,联系人等。

Uri和UriMatch

在学习ContentProvider之前,先了解一下Uri。Uri(Universal Resource Identifier)通用资源标识符,代表要操作的数据。在ContentProvider中,用来表示要操作的ContentProvider和具体的表。

UriMatch用来过滤Uri匹配出具体的操作表。

Uri由authority、path和协议声明组成,authority用于对不同的应用作区分,一般采用包名进行命名。如包名为com.example.app,那么对应的authority可以命名为com.example.app.provider。path用于对同一应用中的不同表作区分,通常加到authority后面,如com.example.app.provider/table1。Uri还需要在字符串头部加上协议声明content。如下,表示包名为“com.example.app“的包下的ContentProvider,其所对应的表table1和table2。

content://com.example.app.provider/table1
content://com.example.app.provider/table2

调用Uri.parse(String)就可以将Uri字符串包装成Uri对象。

ContentProvider的创建和注册

可以通过右键直接创建一个ContentProvider,也可以通过继承ContentProvider实现。需要重写onCreate()getType(Uri)和增删改查等方法。

一般在onCreate()中实现数据库的初始化,其返回值表示数据库初始化是否成功。

getType(Uri)传入一个Uri参数,根据这个参数返回mimeType字符串(这个mimeType就是Intent过滤器中的mineType属性)。ContentProvider的mimeType有固定的格式,如果表示整个表,头部是“vnd.android.cursor.dir/vnd“,如果是单条记录,以”vnd.android.cursor.item/vnd“开头。后面接上authority和表名。

增删改查方法会传入对应的属性,对数据库进行相应的操作,返回操作结果即可。

一个ContentProvider可以处理多个表,也可以处理表的某一条记录。所以一般还会创建一个UriMatcher进行Uri的过滤,以确定操作目标。

示例代码如下:

class SpendProvider : ContentProvider() {
    private val spendDir = 0
    private val spendItem = 1
    private val authority = "com.example.bookkeeping.provider"
    private lateinit var db: SQLiteDatabase

    private val uriMatcher by lazy {
        UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(authority, "spend", spendDir)
            addURI(authority, "spend/#", spendItem)
        }
    }


    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int =
        when (uriMatcher.match(uri)) {
            spendDir -> db.delete("Spend", selection, selectionArgs)
            spendItem -> {
                val spendId = uri.pathSegments[1]
                db.delete("Spend", "id=?", arrayOf(spendId))
            }
            else -> 0
        }


    override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) {
        spendDir -> "vnd.android.cursor.dir/vnd.$authority.spend"
        spendItem -> "vnd.android.cursor.item/vnd.$authority.spend"
        else -> null
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? =
        when (uriMatcher.match(uri)) {
            spendDir, spendItem -> {
                val spendId = db.insert("Spend", null, values)
                Uri.parse("content://$authority/spend/$spendId")
            }
            else -> null
        }


    override fun onCreate(): Boolean = context?.let {
        LitePal.initialize(it)
        db = LitePal.getDatabase()
        true
    } ?: false


    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? = when (uriMatcher.match(uri)) {
        spendDir -> {
            db.query("Spend", projection, selection, selectionArgs, null, null, sortOrder)
        }
        spendItem -> {
            val spendId = uri.pathSegments[1]
            db.query("Spend", projection, "id=?", arrayOf(spendId), null, null, sortOrder)
        }
        else -> null
    }


    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int =
        when (uriMatcher.match(uri)) {
            spendDir -> {
                db.update("Spend", values, selection, selectionArgs)
            }
            spendItem -> {
                val spendId = uri.pathSegments[1]
                db.update("Spend", values, "id=?", arrayOf(spendId))
            }
            else -> 0
        }

}

四大组件都需要在Manifest中进行注册,ContentProvider也不例外。在<provider>标签中,name属性填写组件名,authorities属性填写该组件的标识,即Uri的authority部分,exported默认为true,才能和其他应用进行交互。

注意,在Android11以上使用ContentProvider需要添加< queries>来指定需要和哪些应用进行交互

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.bookkeeping">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
    <queries>
        <package android:name="com.example.contactstest"/>
    </queries>
    
    <application ...>
        <provider
            android:name=".provider.SpendProvider"
            android:authorities="com.example.bookkeeping.provider"
            android:enabled="true"
            android:exported="true" />
    </application>

</manifest>

ContentProvider的使用

当获取了目标ContentProvider的Uri信息后,可以通过context.contentResolver进行交互。 调用contentResolver的增删改查方法,传入Uri和对应的参数,即可实现进程间通信。

class MainActivity : BaseActivity<ActivityMainBinding>() {

    override fun onCreateViewBinding(layoutInflater: LayoutInflater): ActivityMainBinding =
        ActivityMainBinding.inflate(layoutInflater)

    override fun initViews() {
        viewBinding.run {
            arrayOf(
                btnAdd,
                btnDelete,
                btnQuery,
                btnUpdate
            ).forEach { it.setOnClickListener(::onViewClick) }
        }
    }

    private var bookId: String? = null
    private val uri = "content://com.example.contactstest.provider/book"


    private fun onViewClick(v: View) {
        when (v.id) {
            R.id.btn_add -> {
                val value = contentValuesOf(
                    "name" to "A Clash of Kings", "author" to "George Martin",
                    "pages" to 1040, "price" to 22.85
                )
                val newUri = contentResolver.insert(Uri.parse(uri), value)
                bookId = newUri?.pathSegments?.get(1)
            }
            R.id.btn_delete -> {
                bookId?.let {
                    contentResolver.delete(Uri.parse("$uri/$it"), null, null)
                }
            }
            R.id.btn_query -> {
                contentResolver.query(Uri.parse(uri), null, null, null, null)?.apply {
                    while (moveToNext()) {
                        val name = getString(getColumnIndex("name"))
                        val author = getString(getColumnIndex("author"))
                        val pages = getInt(getColumnIndex("pages"))
                        val price = getDouble(getColumnIndex("price"))
                        LogUtils.d(
                            TAG,
                            "book name:$name\nauthor:$author\npages:$pages\nprice:$price"
                        )
                    }
                }
            }
            R.id.btn_update -> {
                bookId?.let {
                    val values = contentValuesOf(
                        "name" to "A Storm of Swords",
                        "pages" to 1216, "price" to 24.05
                    )
                    contentResolver.update(Uri.parse("$uri/$it"), values, null, null)
                }
            }
        }
    }
}