【Android基础】四大组件之ContentProvider

【Android基础】四大组件之ContentProvider

本文介绍了四大组件之ContentProvider的相关内容。

本文中源码来自郭神的《第一行代码》(第三版)。

概念

ContentProvider,即内容提供者,主要用于在不同应用之间共享数据。它提供了一种标准的方式来访问和操作应用程序中的数据。ContentProvider可以被多个应用程序共享,并且可以在不同的应用程序之间进行数据的共享。

ContentProvider的创建与使用

ContentP rovider 的用法一般有两种:

  • 一种是使用现有的ContentProvider 读取和操作相应程序中的数据;

  • 另一种是创建自己的ContentProvider ,给程序的数据提供外部访问接口。

使用现有的ContentP rovider

对于每一个应用程序来说,如果想要访问ContentProvider 中共享的数据,就一定要借助 ContentResolver 类,可以通过 Context 中的 getContentResolver() 方法获取该类的实例。 ContentResolver 中提供了一系列的方法用于对数据进行增删改查操作,其中insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。

ContentResolver 中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给ContentProvider 中的数据建立了唯一标识符,它主要由两部分组成:authority 和path 。authority 是用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名。path 则是用于对同一应用程序中不同的表做区分的,通常会添 加到authority 的后面。

内容URI最标准的格式如下:

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

在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:

val uri = Uri.parse("content://com.example.app.provider/table1")

只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象了。

拿到uri对象之后,就可以调用ContentResolver中的增删改查方法了。

使用cursor对象读取数据时,需要使用moveToFirst()方法将游标移动到第一行数据的位置,然后使用getColumnIndex()方法获取每一列数据的列索引,最后使用getString()方法获取指定列索引对应的数据。

代码如下:

/** uri from table_name 指定查询某个应用程序下的某一张表
projection select column1, column2 指定查询的列名
selection where column = value 指定where的约束条件
selectionArgs - 为where中的占位符提供具体的值
sortOrder order by column1, column2 指定查询结果的排序方式
*/
val cursor = contentResolver.query(
 uri,
 projection,
 selection,
 selectionArgs,
 sortOrder) 

while (cursor.moveToNext()) {
 val column1 = cursor.getString(cursor.getColumnIndex("column1"))
 val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close() 

// 更新数据
val values = ContentValues()
values.put("column1", "value1")
values.put("column2", 1)
val rows = contentResolver.update(
 uri,
 values,
 selection, 
)

// 删除数据
val rows = contentResolver.delete(
 uri,
 selection,
 selectionArgs 
)
// 插入数据
val values = ContentValues()
values.put("column1", "value1")
values.put("column2", 1)
val uri = contentResolver.insert(
 uri,
 values
)

以读取系统联系人为例,代码如下:

private fun readContacts() {
    // 查询联系人数据
    contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        null, null, null, null)?.apply {
        while (moveToNext()) {
            // 获取联系人姓名
            val displayName = getString(getColumnIndex(
                ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
            // 获取联系人手机号
            val number = getString(getColumnIndex(
                ContactsContract.CommonDataKinds.Phone.NUMBER))
            contactsList.add("$displayName\n$number")
        }
        adapter.notifyDataSetChanged()
        close()
    }
} 

注意需要提前检查和申请权限:

<uses-permission android:name="android.permission.READ_CONTACTS" />

运行时权限申请:

public class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main) 
      if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
            != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.READ_CONTACTS), 1)
        } else {
            readContacts()
        }
  }
}

创建自己的ContentProvider

首先在包名文件夹上右键New->Other->Content Provider。设置好ContentProvider的名称和authority,然后点击Finish。就可以在Manifest里看到对应的类和authority了。

然后在ContentProvider类中重写onCreate()、query()、insert()、update()、delete()方法。

<provider
    android:name=".MyContentProvider"
    android:authorities="com.example.app.provider"
    android:exported="true" />

Kotlin代码实现类如下:


class DatabaseProvider : ContentProvider() {
    private val bookDir = 0
    private val bookItem = 1
    private val categoryDir = 2
    private val categoryItem = 3
    private val authority = "com.example.databasetest.provider"
    private var dbHelper: MyDatabaseHelper? = null
    private val uriMatcher by lazy {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "book", bookDir)
        matcher.addURI(authority, "book/#", bookItem)
        matcher.addURI(authority, "category", categoryDir)
        matcher.addURI(authority, "category/#", categoryItem)
        matcher
    }

    override fun onCreate() = context?.let {
        dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)
        true
    } ?: false

    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ) = dbHelper?.let {
        // 查询数据
        val db = it.readableDatabase
        val cursor = when (uriMatcher.match(uri)) {
            bookDir -> db.query(
                "Book", projection, selection, selectionArgs,
                null, null, sortOrder
            )

            bookItem -> {
                val bookId = uri.pathSegments[1]
                db.query(
                    "Book", projection, "id = ?", arrayOf(bookId), null, null,
                    sortOrder
                )
            }

            categoryDir -> db.query(
                "Category", projection, selection, selectionArgs,
                null, null, sortOrder
            )

            categoryItem -> {
                val categoryId = uri.pathSegments[1]
                db.query(
                    "Category", projection, "id = ?", arrayOf(categoryId),
                    null, null, sortOrder
                )
            }

            else -> null
        }
        cursor
    }

    override fun insert(uri: Uri, values: ContentValues?) = dbHelper?.let {
        // 添加数据
        val db = it.writableDatabase
        val uriReturn = 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
        }
        uriReturn
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ) = dbHelper?.let {
        // 更新数据
        val db = it.writableDatabase
        val updatedRows = when (uriMatcher.match(uri)) {
            bookDir -> db.update("Book", values, selection, selectionArgs)
            bookItem -> {
                val bookId = uri.pathSegments[1]
                db.update("Book", values, "id = ?", arrayOf(bookId))
            }

            categoryDir -> db.update("Category", values, selection, selectionArgs)
            categoryItem -> {
                val categoryId = uri.pathSegments[1]
                db.update("Category", values, "id = ?", arrayOf(categoryId))
            }

            else -> 0
        }
        updatedRows
    } ?: 0

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) =
        dbHelper?.let {
            // 删除数据
            val db = it.writableDatabase
            val deletedRows = when (uriMatcher.match(uri)) {
                bookDir -> db.delete("Book", selection, selectionArgs)
                bookItem -> {
                    val bookId = uri.pathSegments[1]
                    db.delete("Book", "id = ?", arrayOf(bookId))
                }

                categoryDir -> db.delete("Category", selection, selectionArgs)
                categoryItem -> {
                    val categoryId = uri.pathSegments[1]
                    db.delete("Category", "id = ?", arrayOf(categoryId))
                }

                else -> 0
            }
            deletedRows
        } ?: 0

    override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
        bookDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
        bookItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
        categoryDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category"
            categoryItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category"
        else -> null
    }
} 

首先,在类的一开始,同样是定义了4个变量,分别用于表示访问Book 表中的所有数据、访问Book 表中的单条数据、访问Category 表中的所有数据和访问Category 表中的单条数据。然后在一个by lazy代码块里对UriMatcher进行了初始化操作,将期望匹配的几种URI格式添加了进去。by lazy代码块是Kotlin 提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当uriMatcher变量首次被调用的时候才会执行,并且会将代码块中最后一行代码的返回值赋给uriMatcher。

使用方的调用:

class MainActivity : AppCompatActivity() {
    var bookId: String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        addData.setOnClickListener {
            // 添加数据
            val uri = Uri.parse("content://com.example.databasetest.provider/book")
            val values = contentValuesOf("name" to "A Clash of Kings",
                "author" to "George Martin", "pages" to 1040, "price" to 22.85)
            val newUri = contentResolver.insert(uri, values)
            bookId = newUri?.pathSegments?.get(1)
        }
        queryData.setOnClickListener {
            // 查询数据
            val uri = Uri.parse("content://com.example.databasetest.provider/book")
            contentResolver.query(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"))
                    Log.d("MainActivity", "book name is $name")
                    Log.d("MainActivity", "book author is $author")
                    Log.d("MainActivity", "book pages is $pages")
                    Log.d("MainActivity", "book price is $price")
                }
                close()
            }
        }
        updateData.setOnClickListener {
            // 更新数据
            bookId?.let {
                val uri = Uri.parse("content://com.example.databasetest.provider/
                        book/$it")
                val values = contentValuesOf("name" to "A Storm of Swords",
                    "pages" to 1216, "price" to 24.05)
                contentResolver.update(uri, values, null, null)
            }
        }
        deleteData.setOnClickListener {
            // 删除数据
            bookId?.let {
                val uri = Uri.parse("content://com.example.databasetest.provider/
                        book/$it")
                contentResolver.delete(uri, null, null)
            }
        }
    }
}

添加数据的时候,首先调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后把要添加的数据都存放到ContentValues对象中,接着调用ContentResolver的insert()方法执行添加操作就可以了。注意,insert()方法会返回一个Uri对象,这个对象中包含了新增数据的id,我们通过getPathSegments()方法将这个id取出,稍后会用到它。

查询数据的时候,同样是调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后调用ContentResolver的query()方法查询数据,查询的结果当然还是存放在Cursor对象中。之后对Cursor进行遍历,从中取出查询结果,并一一打印出来。

更新数据的时候,也是先将内容URI解析成Uri对象,然后把想要更新的数据存放到ContentValues对象中,再调用ContentResolver的update()方法执行更新操作就可以了。注意,这里我们为了不想让Book 表中的其他行受到影响,在调用Uri.parse()方法时,给内容URI的尾部增加了一个id,而这个id正是添加数据时所返回的。这就表示我们只希望更新刚刚添加的那条数据,Book 表中的其他行都不会受影响。

删除数据的时候,也是使用同样的方法解析了一个以id结尾的内容URI,然后调用 ContentResolver的delete()方法执行删除操作就可以了。由于我们在内容URI里指定了一个id,因此只会删掉拥有相应id的那行数据,Book 表中的其他数据都不会受影响。