ContentProvider

简介

ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的 机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使 用ContentProvider是Android实现跨程序共享数据的标准方式。

运行时权限

Android权限机制详解

首先回顾一下过去Android的权限机制。我们在第6章写BroadcastTest项目的时候第一次接触 了Android权限相关的内容,当时为了要监听开机广播,我们在AndroidManifest.xml文件中 添加了这样一句权限声明:

<manifest xmlns:android="http://shemas.android.com/apk/res/android"
packate="com.example.broadcasttest">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
、、、
</manifest>

因为监听开机广播涉及了用户设备的安全,因此必须在AndroidManifest.xml中加入权限声 明,否则我们的程序就会崩溃。

在程序运行时申请权限

首先新建一个RuntimePermissionTest项目,我们就在这个项目的基础上学习运行时权限的使 用方法。我们就使用CALL_PHONE这个权限来作为本小节的示例吧。

接下来修改AndroidManifest.xml文件,在其中声明如下权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.telephony" android:required="true"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
、、、
</manifest>

修改MainActivity中的代码如下所示:

class MainActivity:AppCompatActivity(){
private lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstance)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val makeCall = binding.makeCall
makeCall.setOnClickListener{
//检查权限
if(ContextCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE)!=PackageManager.PERMISSIONO_GRANTED){
//请求权限
ActivityCompat.requestPermissin(this,arrayOf(Manifest.permission.CALL_PHONE),1)
}else{
//拨打电话
call()
}
}

override fun onRequestPermissionResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
){
super.onRequsetPermissionResult(requestCode,permissions,grantResults)
when(requestCode){
1->{
if(grantResults.isNotEmpty()&&grantRsults[0]==PackageManager.PERMISSION_GRANTED){
call()
}else{
Toast.makeText(this,"Your denied the permission",Toast.LENGTH_SHORT).show()
}
}
}
}

private fun call(){
try{
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:13905020820")
startActivity(intent)
}catch(e:SecurityException){
e.printStackTrace()
}
}
}
}

访问其他程序中的数据

ContentResolver的基本用法

如果想访问ContentProvider中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。

内容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对象查询table1表中的数据了,代码如下所示:

val cursor = contentResolver.query(
uri,
projection,
selection,
selectionArgs,
sortOrder)

查询完成后返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取 出来了。读取的思路仍然是通过移动游标的位置遍历Cursor的所有行,然后取出每一行中相应 列的数据,代码如下所示:

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

掌握了最难的查询操作,剩下的增加、修改、删除操作就更不在话下了。我们先来看看如何向 table1表中添加一条数据,代码如下所示:

val values = contentValuesOf("column1" to "text","column2" to 1)
contentResolver.insert(uri,values)

可以看到,仍然是将待添加的数据组装到ContentValues中,然后调用ContentResolver的 insert()方法,将Uri和ContentValues作为参数传入即可

如果我们想要更新这条新添加的数据,把column1的值清空,可以借助ContentResolver的 update()方法实现,代码如下所示:

val valuse = contentValuesOf("column1" to "")
contentResolver.update(uri.values,"column1 = ? and column2 = ?",arrayOf("text","1"))

注意,上述代码使用了selection和selectionArgs参数来对想要更新的数据进行约束,以 防止所有的行都会受影响

最后,可以调用ContentResolver的delete()方法将这条数据删除掉,代码如下所示:

contentResolver.delete(uri,"column2 = ?", arrayOf("1"))

读取系统联系人

新建一个ContactsTest项目,让我们开始动手吧

首先还是来编写一下布局文件,这里我们希望读取出来的联系人信息能够在ListView中显示, 因此,修改activity_main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<ListView
android:id="@+id/contactsView"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>

</LinearLayout>

接着修改MainActivity中的代码,如下所示:

class MainActivity:AppCompatActivity(){
private val contactsList = ArrayList<String>()
private lateinit var adapter = ArrayAdapter<String>
private lateinit var binding :ActivityMainBinding

override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = ArrayAdapter(this,android.R.layout.simple_1,contactsList)
val contactsView = binding.contactsView
if(ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermission(this,arrayOf(Manifest.permission.READ_CONTACTS),1)
}else{
readContacts()
}
}
override fun onRequestPermissionResult(
requsetCode:Int,
permissions:Array<out String>
grantResults:IntArray
){
super.onReausetPermissionResult(requestCode,permissions,grantResults)
when(requestCode){
1->{
if(grantResults.isNotEmpty()&&grantResults[0]==PackageManager.PERMISSION_GRSANTED){
readContacts()
}else{
Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show()
}
}
}
}
@SuppressLint("Range")
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()
}
}

创建自己的ContentProvider

创建ContentProvider的步骤

ContentProvider类中有6个抽象方法,我们在使用子类继承 它的时候,需要将这6个方法全部重写。观察下面的代码示例:

class MyProvider : ContentProvider(){
override fun onCreate():Boolean{
return false
}
override fun query(uri:Uri,projection:Array<String>?,selection:String?,
selectionArgs:Array<String>?,sortOrder:String?):Cursor?{
return null
}
override fun insert(uri:Uri, valuse:ContentValues?,selection:String?,
selectionArgs:Array<String>?):Int{
return 0;
}
override fun update(uri:Uri,values:ContentValues?,selection:String?,
selectionArgs:Array<String>?):Int{
return 0;
}
override fun delete(uri:Uri,secection:String?,selectionArgs:Array<String>?):Int{
return 0;
}
override fun getType(uri:Uri):String?{
return null;
}
}
  • onCreate()。初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider的初始化成功,返回false则表示失败。
  • query()。从ContentProvider中查询数据。uri参数用于确定查询哪张表,projection用于确定查询哪些列,selection和selectionArgs参数用于约定查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
  • insert()。向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,代待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URI
  • update()。更新ContentProvider中已有的数据。uri参数用于确定更新哪一张表中的数据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。
  • delete()。从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
  • getType()。根据传入的内容URI返回相应的MIME类型

一个标准的内容URI写法是:

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

这就表示调用方期望访问的是com.example.app这个应用的table1表中的数据。

除此之外,我们还可以在这个内容URI的后面加上一个id,例如:

content://com.example.app.provier/table1/1

这就表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据。

内容URI的格式主要就只有以上两种,以路径结尾表示期望访问该表中所有的数据,以id结尾表 示期望访问该表中拥有相应id的数据。我们可以使用通配符分别匹配这两种格式的内容URI,规 则如下。

  • *表示匹配任意长度的任意字符
  • #表示匹配任意长度的数字

所以,一个能够匹配任意表的内容RUI格式就可以写成:

content://com.example/app.provider/*

一个能够匹配table1表中任意一行数据的内容URI格式就可以写成:

comtent://com.example.app.provider/table1/#

接着,我们再借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中 提供了一个addURI()方法,这个方法接收3个参数,可以分别把authority、path和一个自 定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断 出调用方期望访问的是哪张表中的数据了。修改MyProvider中的代码,如下所示:

class MyProvider : ContentProvider(){
private val table1Dir = 0
private val table1Item = 1
private val table2Dir = 2
private val table2Item = 3

private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

init{
uriMatcher.addURI("com.example.app.privoider","table1",table1Dir)
uriMatcher.addURI("com.example.app.provider","table1/#",table1Item)
uriMatcher.addURI("com.example.app.provider","table2",table2Dir)
uriMathcer.addURI("com.example.app.provider","table2/#",table2Item)
}

override fun query(ur:Ur,projection:Array<String>,selection:String?,
selectionArgs:Array<String>?,sortOrder:String?):Cursor?{
when(uriMatcher.match(uri)){
table1Dir->{
//查询table1表中的所有数据
}
table1Item->{
//查询table1表中的单条数据
}
table2Dir->{
//查询table2表中的所有数据
}
table2Item->{
//查询table2表中的单挑数据
}
}
}
}

getType方法()。所有ContentProvider都必须提供的一个方法,用于获取Uri对象的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定。

  • 必须以vnd开头
  • 如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/
  • 最后接上vnd..

所以,对于content://com.example.app.provider/table1这个内容URI,它所对应的MIME类型就可以写成:

vnd.android.cursor.dir/vnd.com.example.app.provider.table1

对于content://com.example.app.provider/table1/1这个内容URI,它所对应的MIME类型就可以写成:

vnd.android.cursor.item/vnd.com.example.app.privider.table

现在我们可以继续完善MyProvider中的内容了,这次来实现getType()方法中的逻辑,代码 如下所示:

class MyProvider : ContentProvider(){
override fun getType(uri:Uri) = when(uriMathcer.match(uri)){
table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table"
table2Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
table2Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
else -> null
}
}

到这里,一个完整的ContentProvider就创建完成了,现在任何一个应用程序都可以使用 ContentResolver访问我们程序中的数据。那么,如何才能保证隐私数据不会泄漏出去呢?其 实多亏了ContentProvider的良好机制,这个问题在不知不觉中已经被解决了。因为所有的增 删改查操作都一定要匹配到相应的内容URI格式才能进行,而我们当然不可能向UriMatcher中 添加隐私数据的URI,所以这部分数据根本无法被外部程序访问,安全问题也就不存在了。

实现跨程序数据共享

简单起见,我们还是在上一章中DatabaseTest项目的基础上继续开发,通过ContentProvider 来给它加入外部访问接口。打开DatabaseTest项目,首先将MyDatabaseHelper中使用Toast 弹出创建数据库成功的提示去除,因为跨程序访问时我们不能直接使用Toast。然后创建一个 ContentProvider,右击com.example.databasetest包→New→Other→Content Provider

接着我们修改DatabaseProvider中的代码,如下所示:

class DatabaseProvider : ContentProvider(){
private val bookDir = 0
private val bookItem = 1
private val categoryDir = 2
private val categoryItem = 3
private val authority = "com.example.databasttest.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,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 insert(uri:Uri,valuse: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/$newBookId")
}
category,categoryItem->{
val newCategoryId = db.insert("Category",null,values)
Uri.parse("content://#authority/$newCategoryId")
}
else->null
}
UriReturn
}

override fun update(uri:Uri,values:ContentValuse?,selection:String?,
selectionArgs:Array<String>?) = dbHelper?.let{
//更新数据
val db = it.writableDatbase
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]
dp.update("Category",valuse,id="?",arrayOf(categoryId))
}
else -> 0
}
updateRows
}?:0

override fun delete(uri:Uri,selection:String?,selectionArgs:Array<String>?) = dbHelper.let{
//删除数据
val db = it.writableDatabase
val deleteRows = 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.databesttest.provider.category"
categoryItem->"vnd.android.cursor.item/vnd.com.example.databesttest.provider.category"
else->null
}

}

代码虽然很长,不过不用担心,这些内容都不难理解,因为使用的全部都是上一小节中我们学 到的知识。首先,在类的一开始,同样是定义了4个变量,分别用于表示访问Book表中的所有 数据、访问Book表中的单条数据、访问Category表中的所有数据和访问Category表中的单条 数据。然后在一个by lazy代码块里对UriMatcher进行了初始化操作,将期望匹配的几种 URI格式添加了进去。by lazy代码块是Kotlin提供的一种懒加载技术,代码块中的代码一开始 并不会执行,只有当uriMatcher变量首次被调用的时候才会执行,并且会将代码块中最后一 行代码的返回值赋给uriMatcher。

接下来就是每个抽象方法的具体实现了,先来看一下onCreate()方法。这个方法的代码很 短,但是语法可能有点特殊。这里我们综合利用了Getter方法语法糖、?.操作符、let函 数、?:操作符以及单行代码函数语法糖。首先调用了getContext()方法并借助?.操作符和 let函数判断它的返回值是否为空:如果为空就使用?:操作符返回false,表示 ContentProvider初始化失败;如果不为空就执行let函数中的代码。在let函数中创建了一个 MyDatabaseHelper的实例,然后返回true表示ContentProvider初始化成功。由于我们借 助了多个操作符和标准函数,因此这段逻辑是在一行表达式内完成的,符合单行代码函数的语 法糖要求,所以直接用等号连接返回值即可。其他几个方法的语法结构是类似的,相信你应该 能看得明白。

接着看一下query()方法,在这个方法中先获取了SQLiteDatabase的实例,然后根据传入的 Uri参数判断用户想要访问哪张表,再调用SQLiteDatabase的query()进行查询,并将 Cursor对象返回就好了。注意,当访问单条数据的时候,调用了Uri对象的 getPathSegments()方法,它会将内容URI权限之后的部分以“/”符号进行分割,并把分割后 的结果放入一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的就 是id了。得到了id之后,再通过selection和 selectionArgs参数进行约束,就实现了查询 单条数据的功能。

再往后就是insert()方法,它也是先获取了SQLiteDatabase的实例,然后根据传入的Uri参 数判断用户想要往哪张表里添加数据,再调用SQLiteDatabase的insert()方法进行添加就可 以了。注意,insert()方法要求返回一个能够表示这条新增数据的URI,所以我们还需要调用 Uri.parse()方法,将一个内容URI解析成Uri对象,当然这个内容URI是以新增数据的id结尾 的。

接下来就是update()方法了,相信这个方法中的代码已经完全难不倒你了,也是先获取 SQLiteDatabase的实例,然后根据传入的uri参数判断用户想要更新哪张表里的数据,再调用 SQLiteDatabase的update()方法进行更新就好了,受影响的行数将作为返回值返回

下面是delete()方法,是不是感觉越到后面越轻松了?因为你已经渐入佳境,真正找到窍门 了。这里仍然是先获取SQLiteDatabase的实例,然后根据传入的uri参数判断用户想要删除哪 张表里的数据,再调用SQLiteDatabase的delete()方法进行删除就好了,被删除的行数将作 为返回值返回

最后是getType()方法,这个方法中的代码完全是按照上一节中介绍的格式规则编写的,相信 已经没有解释的必要了。这样我们就将ContentProvider中的代码全部编写完了。

另外,还有一点需要注意,ContentProvider一定要在AndroidManifest.xml文件中注册才可 以使用。不过幸运的是,我们是使用Android Studio的快捷方式创建的ContentProvider,因 此注册这一步已经自动完成了。

现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了,我们赶快来尝试一下。首 先需要将DatabaseTest程序从模拟器中删除,以防止上一章中产生的遗留数据对我们造成干 扰。然后运行一下项目,将DatabaseTest程序重新安装在模拟器上。接着关闭DatabaseTest 这个项目,并创建一个新项目ProviderTest,我们将通过这个程序去访问DatabaseTest中的数 据。

还是先来编写一下布局文件吧,修改activity_main.xml中的代码,如下所示

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<Button
android:id="@+id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Book" />

<Button
android:id="@+id/queryData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query From Book" />

<Button
android:id="@+id/updateData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Book" />

<Button
android:id="@+id/deleteData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete From Book" />

</LinearLayout>

布局文件很简单,里面放置了4个按钮,分别用于添加、查询、更新和删除数据。然后修改 MainActivity中的代码,如下所示:

package com.example.providertest

import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.contentValuesOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.providertest.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

var bookId : String? = null

@SuppressLint("Range")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val addData = binding.addData
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)
Log.d("MainActivity","InsertedURI:$newUri")
}
val queryData = binding.queryData
queryData.setOnClickListener {
val uri = Uri.parse("content://com.example.databasetest.provider/book")
val cursor = contentResolver.query(uri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
do {
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getInt(cursor.getColumnIndex("pages"))
val price = cursor.getDouble(cursor.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")
} while (cursor.moveToNext())
cursor.close()
} else {
Log.d("MainActivity", "No data found.")
}

}
val updateData = binding.updateData
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)
}
}
val deleteData = binding.deleteData
deleteData.setOnClickListener {
bookId?.let {
val uri = Uri.parse("content://com.example.databasetest.provider/book/$it")
contentResolver.delete(uri, null, null)
}
}

}
}

可以看到,我们分别在这4个按钮的点击事件里面处理了增删改查的逻辑。添加数据的时候,首 先调用了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表中的其他数据都不会受影响。

Kotlin课堂:泛型和委托

泛型的基本用法

首先解释一下什么是泛型。在一般的编程模式下,我们需要给任何一个变量指定一个具体的类 型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好 的扩展性。

举个例子,List是一个可以存放数据的列表,但是List并没有限制我们只能存放整型数据或字符 串数据,因为它没有指定一个具体的类型,而是使用泛型来实现的。也正是如此,我们才可以 使用List、List之类的语法来构建具体类型的列表。

那么要怎样才能定义自己的泛型实现呢?这里我们来学习一下基本的语法。

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是 。当然括号内的T并不是固定要求的,事实上你使用任何英文字母或单词都可以,但是通常 情况下,T是一种约定俗成的泛型写法。

如果我们要定义一个泛型类,就可以这么写

class MyClass<T> { 

fun method(param: T): T {
return param
}

}

此时的MyClass就是一个泛型类,MyClass中的方法允许使用T类型的参数和返回值。

我们在调用MyClass类和method()方法的时候,就可以将泛型指定成具体的类型,如下所 示:

val myClass = MyClass<Int>() 
val result = myClass.method(123)

这里我们将MyClass类的泛型指定成Int类型,于是method()方法就可以接收一个Int类型的 参数,并且它的返回值也变成了Int类型。

而如果我们不想定义一个泛型类,只是想定义一个泛型方法,应该要怎么写呢?也很简单,只 需要将定义泛型的语法结构写在方法上面就可以了,如下所示:

class MyClass { 

fun <T> method(param: T): T {
return param
}

}

此时的调用方式也需要进行相应的调整

val myClass = MyClass() 
val result = myClass.method<Int>(123)

可以看到,现在是在调用method()方法的时候指定泛型类型了。另外,Kotlin还拥有非常出色 的类型推导机制,例如我们传入了一个Int类型的参数,它能够自动推导出泛型的类型就是Int 型,因此这里也可以直接省略泛型的指定:

val myClass = MyClass() 
val result = myClass.method(123)

Kotlin还允许我们对泛型的类型进行限制。目前你可以将method()方法的泛型指定成任意类 型,但是如果这并不是你想要的话,还可以通过指定上界的方式来对泛型的类型进行约束,比 如这里将method()方法的泛型上界设置为Number类型,如下所示:

class MyClass { 

fun <T : Number> method(param: T): T {
return param
}

}

另外,在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的 时候,泛型的上界默认是Any?。而如果想要让泛型的类型不可为空,只需要将泛型的上界手动 指定成Any就可以了

接下来,我们尝试对本小节所学的泛型知识进行应用。回想一下,在6.5.1小节学习高阶函数的 时候,我们编写了一个build函数,代码如下所示:

fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder { 
block()
return this
}

这个函数的作用和apply函数基本是一样的,只是build函数只能作用在StringBuilder类上 面,而apply函数是可以作用在所有类上面的。现在我们就通过本小节所学的泛型知识对build函数进行扩展,让它实现和apply函数完全一样的功能。

思考一下,其实并不复杂,只需要使用将build函数定义成泛型函数,再将原来所有强制指 定StringBuilder的地方都替换成T就可以了。新建一个build.kt文件,并编写如下代码:

fun <T> T.build(block: T.() -> Unit): T { 
block()
return this
}

大功告成!现在你完全可以像使用apply函数一样去使用build函数了,比如说这里我们使用 build函数简化Cursor的遍历:

contentResolver.query(uri, null, null, null, null)?.build { 
while (moveToNext()) {
...
}
close()
}

类委托和委托属性

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委 托给另外一个辅助对象去处理。

Kotlin中也是支持委托功能的,并且将委托功能分为了两种:类委托和委托属性。下面我们逐个 进行学习。

首先来看类委托,它的核心思想在于将一个类的具体实现委托给另一个类去完成。在前面的章 节中,我们曾经使用过Set这种数据结构,它和List有点类似,只是它所存储的数据是无序 的,并且不能存储重复的数据。Set是一个接口,如果要使用它的话,需要使用它具体的实现 类,比如HashSet。而借助于委托模式,我们可以轻松实现一个自己的实现类。比如这里定义 一个MySet,并让它实现Set接口,代码如下所示:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> { 

override val size: Int
get() = helperSet.size

override fun contains(element: T) = helperSet.contains(element)

override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)

override fun isEmpty() = helperSet.isEmpty()

override fun iterator() = helperSet.iterator()

}

可以看到,MySet的构造函数中接收了一个HashSet参数,这就相当于一个辅助对象。然后在 Set接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实 现,这其实就是一种委托模式

那么,这种写法的好处是什么呢?既然都是调用辅助对象的方法实现,那还不如直接使用辅助 对象得了。这么说确实没错,但如果我们只是让大部分的方法实现调用辅助对象中的方法,少 部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么MySet就会成为一个全新 的数据结构类,这就是委托模式的意义所在

但是这种写法也有一定的弊端,如果接口中的待实现方法比较少还好,要是有几十甚至上百个 方法的话,每个都去这样调用辅助对象中的相应方法实现,那可真是要写哭了。那么这个问题 有没有什么解决方案呢?在Java中确实没有,但是在Kotlin中可以通过类委托的功能来解决。

Kotlin中委托使用的关键字是by,我们只需要在接口声明的后面使用by关键字,再接上受委托 的辅助对象,就可以免去之前所写的一大堆模板式的代码了,如下所示:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet { 
}

这两段代码实现的效果是一模一样的,但是借助了类委托的功能之后,代码明显简化了太多。 另外,如果我们要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方 法仍然可以享受类委托所带来的便利,如下所示:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet { 

fun helloWorld() = println("Hello World")

override fun isEmpty() = false

}

类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将 一个属性(字段)的具体实现委托给另一个类去完成。

我们看一下委托属性的语法结构,如下所示:

class MyClass { 
var p by Delegate()
}

可以看到,这里使用by关键字连接了左边的p属性和右边的Delegate实例,这是什么意思呢? 这种写法就代表着将p属性的具体实现委托给了Delegate类去完成。当调用p属性的时候会自 动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的 setValue()方法。

因此,我们还得对Delegate类进行具体的实现才行,代码如下所示:

class Delegate { 

var propValue: Any? = null

operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
return propValue
}

operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
propValue = value
}

}

这是一种标准的代码实现模板,在Delegate类中我们必须实现getValue()和setValue()这 两个方法,并且都要使用operator关键字进行声明。 getValue()方法要接收两个参数:第一个参数用于声明该Delegate类的委托功能可以在什么 类中使用,这里写成MyClass表示仅可在MyClass类中使用;第二个参数KProperty<*>是 Kotlin中的一个属性操作类,可用于获取各种属性相关的值,在当前场景下用不着,但是必须在 方法参数上进行声明。另外,<*>这种泛型的写法表示你不知道或者不关心泛型的具体类型,只 是为了通过语法编译而已,有点类似于Java中的写法。至于返回值可以声明成任何类型,根 据具体的实现逻辑去写就行了,上述代码只是一种示例写法。 setValue()方法也是相似的,只不过它要接收3个参数。前两个参数和getValue()方法是相 同的,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方 法返回值的类型保持一致。 整个委托属性的工作流程就是这样实现的,现在当我们给MyClass的p属性赋值时,就会调用 Delegate类的setValue()方法,当获取MyClass中p属性的值时,就会调用Delegate类的 getValue()方法。是不是很好理解? 不过,其实还存在一种情况可以不用在Delegate类中实现setValue()方法,那就是 MyClass中的p属性是使用val关键字声明的。这一点也很好理解,如果p属性是使用val关键 字声明的,那么就意味着p属性是无法在初始化之后被重新赋值的,因此也就没有必要实现 setValue()方法,只需要实现getValue()方法就可以了。

实现一个自己的lazy函数

那么学习了Kotlin的委托功能之后,我们就可以对by lazy的工作原理进行解密了,它的基本 语法结构如下:

val p by lazy{  }

现在再来看这段代码,是不是觉得更有头绪了呢?实际上,by lazy并不是连在一起的关键 字,只有by才是Kotlin中的关键字,lazy在这里只是一个高阶函数而已。在lazy函数中会创建 并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的 getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式,这样 表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行 代码的返回值。

这样看来,Kotlin的懒加载技术也并没有那么神秘,掌握了它的实现原理之后,我们也可以实现 一个自己的lazy函数。

那么话不多说,开始动手吧。新建一个Later.kt文件,并编写如下代码

class Later<T>(val block: () -> T) { 
}

这里我们首先定义了一个Later类,并将它指定成泛型类。Later的构造函数中接收一个函数 类型参数,这个函数类型参数不接收任何参数,并且返回值类型就是Later类指定的泛型

接着我们在Later类中实现getValue()方法,代码如下所示:

class Later<T>(val block: () -> T) { 

var value: Any? = null

operator fun getValue(any: Any?, prop: KProperty<*>): T {
if (value == null) {
value = block()
}
return value as T
}

}

由于懒加载技术是不会对属性进行赋值的,因此这里我们就不用实现setValue()方法了。