抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >
海灵 · 文章导读
00:00 / 00:00
听一听这篇文章

先说结论

海灵提示:今天带大家学习Android跨程序共享数据的标准方式ContentProvider。在学习它之前,我们必须掌握Android的运行时权限机制。文中详细区分了自动授权的普通权限与需要用户同意的危险权限,并以申请打电话权限为例,展示了如何动态检查、申请权限以及处理授权结果回调的具体代码实现。

课本

书籍资源进入官网下载,PC端进入

第八章-跨程序共享数据,探究ContentProvider

在上一章中不管何种方式实现数据持久化,都只能保存在当前应用程序中访问.而自Android4.2后推荐使用ContentProvide技术.
共享数据的应用场景一般有通讯录,短信等,如果这些数据都不允许第三方程序进行访问的话,恐怕很多应用的功能就要大打折扣了。

ContentProvider简介

ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使用ContentProvider是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,ContentProvider可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
不过,在正式开始学习ContentProvider之前,我们需要先掌握另外一个非常重要的知识——Android运行时权限,因为待会的ContentProvider示例中会用到运行时权限的功能。当然,不光是ContentProvider,以后我们的开发过程中会经常使用运行时权限,因此你必须能够牢牢掌握它才行。

运行时权限

Android权限机制详解

上一次了解权限机制还是在(雾,(^_^)上一次))第6章为了获得权限我们需要在AndroidManifest.xml中声明权限,然后在系统安装程序就会提示程序所需要的权限,从而决定是否要安装.

Android运行时权限流程示意

这种权限机制的设计思路其实非常简单,就是用户如果认可你所申请的权限,就会安装你的程序,如果不认可你所申请的权限,那么拒绝安装就可以了。
但是理想是美好的,现实却很残酷。很多我们离不开的常用软件普遍存在着滥用权限的情况,不管到底用不用得到,反正先把权限申请了再说。
像微信这样的厂商很容易店大欺客.为了应付这种情况在Android6.0中加入了运行时权限,也就是在软件的使用过程中申请某一个权限.
当然也不是所有权限在运行时都需要申请,所有限制Android将权限分为了普通权限和危险权限,准确的说还有一个特殊权限.

  • 普通权限: 系统会帮我们进行自动授权
  • 危险权限: 比如获取联系人等,需要用户自己同意申请.

到Android10为止,危险权限共有11组30个,其余的大部分就是普通权限.
这个表仅作为参照表即可,在实际开发中若是在这张表中查到就在AndroidManifest.xml声明权限就可以了.

运行时权限

本小节将使用CALL_OHONE这个权限作为示例

  1. 新建RuntimePermissionTest项目
  2. 因为打电话涉及到用户的话费资费问题,所以被列为危险权限.修改activity_main.xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/makeCall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="打电话"/>
</LinearLayout>
  1. 接下来修改AndroidManifest.xml文件,在其中声明如下权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CALL_PHONE"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RuntimePermissionTest"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>

</manifest>
  1. 接着修改MainActivity中的代码,如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val makeCall = findViewById<Button>(R.id.makeCall)
makeCall.setOnClickListener {
// 检查是否授予打电话的权限,if中检查权限只要不是0就表示有权限
/*checkSelfPermission接收两个参数
* param1: 上下文Context
* param2: 具体的权限名
* */
if (ContextCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
// 请求授予权限
/*requestPermissions用于请求权限,接收三个参数
* param1 : Activity的实例
* param2 : 权限名数组
* param3 : 请求码,任意的唯一值即可
* */
ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CALL_PHONE), 1)
} else {
// 直接打电话
call()
}
}

}

private fun call() {
// 这个try主要防止卡bug权限获取失败
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}catch (e: IllegalStateException) {
e.printStackTrace()
}
}

// 调用requestPermissions会触发此方法
override fun onRequestPermissionsResult(
// 在requestPermissions(Activity, String[], int)中传递的请求代码
requestCode: Int,
// 请求的权限。
permissions: Array<out String>,
// 对应的权限的授予结果
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 ->{
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED){
call()
}else {
Toast.makeText(this, "权限获取失败", Toast.LENGTH_SHORT).show()
}
}
}
}
}

上面的代码意思就是当用户点击按钮的时候先检查权限是否已经授予了.若已经授权则直接调用call()方法.没有的话就调用ActivityCompat.requestPermissions()方法向用户申请授权。
调用完requestPermissions()方法之后,系统会弹出一个权限申请的对话框,用户可以选择同意或拒绝我们的权限申请。不论是哪种结果,最终都会回调到onRequestPermissionsResult()方法中,而授权的结果则会封装在grantResults参数当中。这里我们只需要判断一下最后的授权结果:如果用户同意的话,就调用call()方法拨打电话;如果用户拒绝的话,我们只能放弃操作,并且弹出一条失败提示。
5. 现在重新运行一下程序,并点击打电话按钮,就可以看到系统弹出的运行时权限申请对话框。

好了,关于运行时权限的内容就讲到这里,现在你已经有能力处理Android上各种关于权限的问
题了,下面我们就来进入本章的正题——ContentProvider。

本小节代码

访问其他程序中的数据

在应用程序中使用ContentProvider中共享的数据,就需要使用在Context中的getContentResolver()方法获取该类的实例.
ContentResolver中提供了一下方法用于读写数据:

  • insert()方法用于添加数据
  • update()方法用于更新数据
  • delete()方法用于删除数据
  • query()方法用于查询数据
  1. insert()增加数据代码示例如下:
val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uri, values)

可以看到,仍然是将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入即可。
3. update()更新数据代码示例如下:

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

上述代码使用了selection和selectionArgs参数来对想要更新的数据进行约束,以防止所有的行都会受影响。
4. delete()删除数据代码示例如下:

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

读取系统联系人

  1. 在编写代码前先手动添加几个人联系人.

ContentResolver读取联系人数据示意

  1. 新建ContactsTest项目,修改activity_main.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
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>

这个布局文件中只放置了一个ListView控件
3. 接着修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {
// 联系人列表
private val contactsList = ArrayList<String>()
// ListView适配器
private lateinit var adapter : ArrayAdapter<String>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val contactsView = findViewById<ListView>(R.id.contactsView)
// 设置适配器布局和数据
adapter = ArrayAdapter(this,android.R.layout.simple_list_item_1,contactsList)
// 加载适配器
contactsView.adapter = adapter
// 判断是否有读取联系人的权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)!=PackageManager.PERMISSION_GRANTED){
// 没有权限则申请权限
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS),1)
}else{
readContacts()
}
}

// 请求权限时的事件
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// 请求电话权限的标识码是1
when(requestCode) {
1 -> {
// 再次验证权限
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED){
readContacts()
}else{
Toast.makeText(this, "权限获取失败", Toast.LENGTH_SHORT).show()
}
}
}
}

private fun readContacts() {
// 这里的程序包名和路径使用的是安卓提供的URI
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null)?.apply {
// 循环查询结果
while (moveToNext()){
val displayName = getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val number = getString(getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
contactsList.add("$displayName\n$number")
}
// 告诉adapter该刷新数据了
adapter.notifyDataSetChanged()
// 关闭查询
close()
}
}
}

在onCreate()方法中,对ListView初始化,然后调用运行时权限的处理逻辑,因为READ_CONTACTS权限属于危险权限。这里我们在用户授权之后,调用readContacts()方法读取系统联系人信息。
在readContacts()方法可以看到,使用了ContentResolver的query()方法查询系统的联系人数据。ContactsContract.CommonDataKinds.Phone类已经帮我们做好了封装,提供了一个CONTENT_URI常量。
接着我们对query()方法返回的Cursor对象进行遍历,这里使用了?.操作符和apply函数来简化遍历的代码。在apply函数中将联系人姓名和手机号逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,联系人手机号这一列对应的常量是ContactsContract.CommonDataKinds.Phone.NUMBER。将两个数据取出后进行拼接,并且在中间加上换行符,然后将拼接后的数据添加到ListView的数据源里,并通知刷新一下ListView,最后千万不要忘记将Cursor对象关闭。
4. 读取系统联系人的权限千万不能忘记声明,修改AndroidManifest.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

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

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ContactsTest"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>

</manifest>

加入了android.permission.READ_CONTACTS权限,这样我们的程序就可以访问系统的联系人数据了。现在终于大功告成
5. 让我们来运行一下程序吧,ListView中会显示读取到的联系人姓名。

本小节代码

创建自己的ContentProvider

从上面的例子可以知道,访问其它程序的数据只要获得应用程序的内容URI,然后借助ContentResolver进行增删改查操作就可以了。那么接下来我们将学习是如何实现这种功能的,他又是怎么保证数据安全的.

创建ContentProvider的步骤

前面提到过,要想实现程序共享数据的功能,可以自己实现一个类去继承ContentProvider的方法来实现.

  1. 继承ContentProvider需要重写6个抽象方法,代码如下:
class MyProvider : ContentProvider() {
// 初始化ContentProvider时调用的,通常用于数据库的创建和升级操作,返回true表示ContentProvider初始化成功,false失败。
override fun onCreate(): Boolean {
TODO("Not yet implemented")
}

// 从ContentProvider中查询数据,查询结果放置到Cursor(游标)对象返回。
override fun query(
// 确定查询的表
uri: Uri,
// 确定查询的列
projection: Array<out String>?,
// 约束条件
selection: String?,
// 约束条件的具体值
selectionArgs: Array<out String>?,
// 排序
sortOrder: String?
): Cursor? {
TODO("Not yet implemented")
}

// 根据传入的URI返回相应的MIME类型
override fun getType(uri: Uri): String? {
TODO("Not yet implemented")
}

// 向ContentProvider中添加一条数据,返回添加的新记录的URI
override fun insert(
// 要添加的表
uri: Uri,
// 待添加的数据
values: ContentValues?): Uri? {
TODO("Not yet implemented")
}

// 从ContentProvider中删除数据,返回被删除的行数
override fun delete(
// 确定被删除的表名
uri: Uri,
// 约束条件
selection: String?,
// 约束具体值
selectionArgs: Array<out String>?): Int {
TODO("Not yet implemented")
}

// 更新ContentProvider中的数据,返回受影响行数
override fun update(
// 确定添加的表名
uri: Uri,
// 待添加的数据
values: ContentValues?,
// 约束条件
selection: String?,
// 约束条件的具体值
selectionArgs: Array<out String>?
): Int {
TODO("Not yet implemented")
}
}

可以发现,很多方法里面都要uri这个参数,这个方法也是调用ContentProvider的增删改查方法传递来的.
2. 判断调用方期望访问的是那张表的数据,修改MyProvider中的代码

class MyProvider : ContentProvider() {

// 关键代码开始
// 定义表的内部数据的Id
private val table1Dir = 0
private val table1Item = 1
private val table2Dir = 2
private val table2Item =3

// 助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

init {
// 包名 表名 数据Id
uriMatcher.addURI("com.example.app.provider", "table1", table1Dir)
// 包名 表名 #表示只获取一行 数据Id
uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item)
/*UriMatcher中提供了一个addURI()方法,这个方法接收3个参数
* authority: 包名
* path: 要匹配的路径。*可以用作任何文本的通配符,#可以用作数字的通配符。
* code: 自定义代码
* */
uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir)
uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item)
}

override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
/*UriMatcher.match()方法尝试匹配url中的路径。返回匹配节点的自定义代码(使用addURI添加),如果没有匹配节点则为-1。
* uri: 我们将匹配的路径uri。
* */
when (uriMatcher.match(uri)){
table1Dir -> TODO("查询table1表中的所有数据")
table1Item -> TODO("查询table1表中单条数据")
table2Dir -> TODO("查询table2表中的所有数据")
table2Item -> TODO("查询table2表中单条数据")
}
TODO("Not yet implemented")
}

// 关键代码结束
override fun onCreate(): Boolean { TODO("Not yet implemented")}
override fun getType(uri: Uri): String? { TODO("Not yet implemented")}
override fun insert( uri: Uri, values: ContentValues?): Uri? { TODO("Not yet implemented")}
override fun delete( uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int { TODO("Not yet implemented")}
override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int { TODO("Not yet implemented")}
}

在分析代码前,值得一提的是前面标准的URI写法是:

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

如果要访问table1表中id为1的数据则应该这样写

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

如果想要访问表中任意数据的URI格式为:

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

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

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

了解了URI的基本规则,接下来我们再看代码.我们预先定义了4个整形变量.接着我们在MyProvider类实例化的时候就创建了UriMatcher的实例,并调用addURI()方法.当query()被调用的时候,就会通过UriMatcher的match()方法对传入的uri进行匹配,如果匹配的就会返回自定义的代码,接着判断期望访问的什么数据.

  1. 上面介绍了query(),其实insert(),insert()、update()、delete()这几个方法的实现是差不多的.除此之外还有一个getType()方法,它是所有ContentProvider都必须提供的方法用于获取Uri对象所对应的MIME类型.
    一个内容URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定。
  • 必须以vnd开头
  • 如果内容URI以路径结尾,则在vnd后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/。
  • 最后接上vnd.Uri。
    所以对于content://com.example.app.provider/table1的MIME类型就可以写成vnd.android.cursor.dir/vnd.com.example.app.provider.table1
  1. 接着我们就可以完善MyProvider中的内容了,这次来实现getType()方法中的逻辑,代码
    如下所示:
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.provider", "table1", table1Dir)
uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item)
uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir)
uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item)
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
when (uriMatcher.match(uri)) {
table1Dir -> TODO("查询table1表中的所有数据")
table1Item -> TODO("查询table1表中单条数据")
table2Dir -> TODO("查询table2表中的所有数据")
table2Item -> TODO("查询table2表中单条数据")
}
TODO("Not yet implemented")
}
override fun onCreate(): Boolean {
TODO("Not yet implemented")
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
TODO("Not yet implemented")
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
TODO("Not yet implemented")
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
TODO("Not yet implemented")
}

// 关键代码开始
override fun getType(uri: Uri):String? = when(uriMatcher.match(uri)){
table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
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,所以这部分数据根本无法被外部程序访问,安全问题也就不存在了。

实现跨程序数据共享

好了,创建ContentProvider的步骤在上面已经清楚了,下面就来实战一下,真正体验一回跨程序数据共享的功能。

  1. 打开前面的DatabaseTest项目
  2. 创建一个ContentProvider名字为DatabaseProvider,右击com.example.databasetest包→New→Other→ContentProvider。整体共享关系可以理解成下面这样:

自定义ContentProvider跨程序共享数据示意

可以看到,我们将ContentProvider命名为DatabaseProvider,将authority指定为com.example.databasetest.provider,Exported属性表示是否允许外部程序访问我们的ContentProvider,Enabled属性表示是否启用这个ContentProvider。将两个属性都勾中,点击“Finish”完成创建。
3. 接着我们修改DatabaseProvider中的代码,如下所示:

class DatabaseProvider : ContentProvider() {

// 访问Book表中所有的数据
private val bookDir = 0
// 访问单条
private val bookItem = 1
// 访问Category表中的所有数据
private val categoryDir = 2
// 访问Category中的单条数据
private val categoryItem = 3
// 定义Uri的authority
private val authority = "com.example.databasetest.provider"
// 预定义
private lateinit var dbHelper : MyDatabaseHelper
// 对UriMatcher进行初始化操作
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
}

// 同样的获得SQLiteDatabase对象
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]
// 这里是返回一个受影响的int行数
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
}
deleteRows
}?: 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
}

// 同样的获得SQLiteDatabase对象
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)
// 因为insert()方法需要返回一个新增数据的URI,所以需要转换一下
Uri.parse("content://$authority/book/$newBookId")
}
categoryDir,categoryItem -> {
val newCategoryId = db.insert("Category",null,values)
Uri.parse("content://$authority/category/$newCategoryId")
}
else -> null
}
uriReturn
}
// 先看看上下文(context)有没有东西,如果没有东西就返回false,表示ContentProvider初始化失败。
override fun onCreate() = context?.let {
// 实例化SQLiteOpenHelper
dbHelper = MyDatabaseHelper(it,"BookStore.db",2)
// 直接返回true表示初始化成功
true
} ?: false

override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
) = dbHelper?.let {
// 获得SQLiteDatabase对象
val db = it.readableDatabase
// 创建一个查询结果cursor变量,根据匹配uri来判断访问的那张表
val cursor = when (uriMatcher.match(uri)){
// 访问所有数据,直接调用SQLiteDatabase的query()方法查询,返回一个Cursor对象
bookDir -> db.query("Book", projection, selection, selectionArgs,null,null,sortOrder)
// 访问单条数据
bookItem -> {
// 调用requestPermissions()方法,URI权限后的部分以“/”分割,且保存到一个字符串列表
// 这个列表的第0个位置存放的就是路径,第1个位置存放的就是id了
val bookId = uri.pathSegments[1]
// 接着再查询表,表名 传入的查询列 约束条件 取出的查询Id 排序
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
cursor
}

override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
) = dbHelper?.let {
val db = it.writableDatabase
val updateRows = when (uriMatcher.match(uri)) {
bookDir -> db.update("Book",values,selection,selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
// update需要返回受影响的行数
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
}
updateRows
} ?: 0
}

肉眼可见的,代码非常的长,但是大部分都是重复或者类似的处理逻辑所以理解起来并不难.
在类的开始定义的四个变量分别表示两个表的单条和所有数据.然后再定义了一个变量uriMatcher使用by lazy进行初始化操作,by lazy是一种Kotlin提供的懒加载技术,代码块中的技术一开始并不会被执行,只有当调用它首次被调用时才会被执行,且最后一行会被返回给变量uriMatcher,具体的介绍将在本章Kotlin讲解
调用该类onCreate()会被首先触发,首先调用了getContext()方法并借助?.操作符和let函数判断它的返回值是否为空:如果为空就使用?:操作符返回false,表示ContentProvider初始化失败;而不为空执行的代码结合注释理解起来应该没问题.
接着query()方法内部逻辑处理完毕后需要返回一个Cursor对象,update()方法返回受影响的行数
insert()和delete()都先获取了SQLiteDatabase的实例,不过inert()需要返回新增对象的Uri,delete()返回被删除的行数
最后是getType()方法,这个方法中的代码完全是按照上一节中介绍的格式规则编写的
4. 另外,还有一点需要注意,ContentProvider一定要在AndroidManifest.xml文件中注册才可以使用。不过幸运的是,我们是使用Android Studio的快捷方式创建的ContentProvider,因此注册这一步已经自动完成了。打开AndroidManifest.xml文件瞧一瞧,如下所示:
可以看到,<application>标签内出现了一个新的标签<provider>,我们使用它来对DatabaseProvider进行注册。android:name属性指定了DatabaseProvider的类名,android:authorities属性指定了DatabaseProvider的authority,而enabled和exported属性则是根据我们刚才勾选的状态自动生成的,这里表示允许DatabaseProvider被其他应用程序访问。

  1. 本小节代码

尝试跨程序数据共享

完成上面的代码后现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了,我们赶快来尝试一下.

  1. 首先需要将DatabaseTest程序从模拟器中删除,以防止上一章中产生的遗留数据对我们造成干扰。
  2. 然后运行一下项目,将DatabaseTest程序重新安装在模拟器上.
  3. 接着关闭DatabaseTest这个项目,并创建一个新项目ProviderTest,我们将通过这个程序去访问DatabaseTest中的数据。
  4. 修改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="添加到Book表" />
<Button
android:id="@+id/queryData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="查询Book表" />
<Button
android:id="@+id/updateData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="更新Book表" />
<Button
android:id="@+id/deleteData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="删除Book表" />
</LinearLayout>

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

class MainActivity : AppCompatActivity() {

private var bookId : String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val addData = findViewById<Button>(R.id.addData)
val deleteData = findViewById<Button>(R.id.deleteData)
val updateData = findViewById<Button>(R.id.updateData)
val queryData = findViewById<Button>(R.id.queryData)

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(getColumnIndexOrThrow("name"))
val author = getString(getColumnIndexOrThrow("author"))
val pages = getString(getColumnIndexOrThrow("pages"))
val price = getString(getColumnIndexOrThrow("price"))
Log.d("MainActivity","book的名字是$name")
Log.d("MainActivity","book的作者是$author")
Log.d("MainActivity","book的页数是$pages")
Log.d("MainActivity","book的价格是$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 {
// 删除数据
Log.d("MainActivity","it是$it")
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表中的其他数据都不会受影响。
6. 如果使用安卓10(Api29)以上版本或遇到Unknown URL content,添加以下代码.
修改ProviderTest的AndroidManifest.xml文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!--指定databasetest对本应用可见-->
<queries>
<package android:name="com.example.databasetest" />
<!-- 也可以单独指定provider -->
<!--<provider android:authorities="com.zhouzhou.databasetest.provider" />-->
</queries>

<application
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ProviderTest"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>

</manifest>
  1. 现在运行一下ProviderTest项目,GIF录制有限制,具体效果建议自己尝试。
    由此可以看出,我们的跨程序共享数据功能已经成功实现了!现在不仅是ProviderTest程序,任何一个程序都可以轻松访问DatabaseTest中的数据,而且是我们自己定义的SQL语句,所以丝毫不用担心隐私数据泄漏的问题。
  2. 本小节代码ProviderTest

Kotlin课堂:泛型和委托

泛型的基本用法

泛型这个概念在Java1.5版本就引入了,Kotlin在Java原有的基础上做了衍生.本小节先了解和Java相同的部分.
首先解释一下什么是泛型。在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。

  1. 定义泛型就是在原有定义类型的地方用字母替代,约定俗成的方法一般使用T,定义泛型有两种定义方式:
    • 泛型类
    class MyClass<T>{
    fun method(name: T):T = name
    }

    此时的MyClass就是一个泛型类,MyClass中的方法允许使用T类型的参数和返回值。我们在调用MyClass类和method()方法的时候,就可以将泛型指定成具体的类型,如下所示:

    val myClass = MyClass<String>()
    val name = myClass.method("小王")

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

    • 泛型方法
      想定义一个泛型方法,应该要怎么写呢?也很简单,只需要将定义泛型的语法结构写在方法上面就可以了,如下所示:
    fun <T> myMethod(name: T):T = name

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

    val name = myMethod<String>("小明")

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

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

这种写法就表明,我们只能将method()方法的泛型指定成数字类型,比如Int、Float、Double等。但是如果你指定成字符串类型,就肯定会报错,因为它不是一个数字。
默认情况下,泛型都可以指定成null类型,如果不想泛型为null,则需要指定泛型的上界为任意类型或Any
3. 回想一下,在6.5.1小节学习高阶函数的时候,我们编写了一个build函数,代码如下所示:

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

这个函数的作用和apply函数基本是一样的,只是build函数只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的。现在我们就通过本小节所学的泛型知识对build函数进行扩展,让它实现和apply函数完全一样的功能。
实现起来只需要使用<T>将build函数定义成泛型函数,再将原来所有强制指定StringBuilder的地方都替换成T就可以了,代码如下:

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

类委托和委托属性

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

  • 类委托:类委托的核心思想就是将自己的实现交给其它类去完成.
    这里我们来实现一个简单的类委托,代码如下:
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接口.接着再重写Set的方法,在重写的方法中没有我们自己的代码,全部都是调用的HashSet中的方法.可以看见Set接口的实现委托给了HashSet,这其实就是一种委托模式.
既然都是调用别的类的方法,那么为什么不直接使用别的类呢?它的意义在于我们可以让大部分的方法调用别的类的方法,少部分自己重写,加入自己独有的方法,那么该方法就会成为一个全新的类.
但是在上面的例子中,细心的同学可能发现只有几个方法还好,如果有几十上百个方法,那么一个一个去调用别的类的方法岂不得累死.其实在Kotlin中可以通过类委托的功能来解决问题
Kotlin中委托使用的关键字是by,我们只需要在接口声明的后面使用by关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式的代码了,如下所示:

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

这段代码将MySet委托给了helperSet,MySet会自动拥有helperSet中已有的方法.如果要新增或重写某个方法,就直接重写就好.

  • 属性委托:
    类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。
    我们看一下委托属性的语法结构,如下所示:
fun main() {
var p by Delegate()
}

class Delegate {
operator fun getValue(nothing: Nothing?, property: KProperty<*>): Any {
TODO("Not yet implemented")
}

operator fun setValue(nothing: Nothing?, property: KProperty<*>, any: Any) {
TODO("Not yet implemented")
}
}

这里使用by关键字连接了左边的p属性和右边的Delegate实例,这种就代表着将属性p的实现交给Delegate类去完成.当调用Delegate类的getValue(),当赋值时会调用Delegate类的setValue()方法.
接下来对Delegate进行具体的实现,代码如下:

class Delegate {
private var propValue: Any? = null

/* getValue()方法要接收两个参数
* param1: 声明该类的委托可以在什么类中使用
* param2:用于获取各种属性的值,这里泛型为*表示不关心泛型的具体类型,类似Java的<?>
* return: 任何值
* */
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = propValue
/* setValue()方法也是相似的,只不过它要接收3个参数。
* param1: 声明该类的委托可以在什么类中使用
* param2:用于获取各种属性的值,这里泛型为*表示不关心泛型的具体类型,类似Java的<?>
* param3: 具体要赋值给委托属性的值,必须和getValue()的返回值类型一致
* */
operator fun setValue(nothing: Nothing?, property: KProperty<*>, any: Any?) {
propValue = any
}
}

这是一种标准的代码实现模板,在Delegate类中我们必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。
如果定义属性p的时候使用的val关键字就可以忽略setValue()方法,因为他是只读的.

实现一个自己的lazy函数

在实现跨程序数据共享初始化uriMatcher变量的时候,把想要延迟执行的代码放到by lazy代码块中,这样代码块中的代码在一开始的时候就不会执行,只有当uriMatcher变量首次被调用的时候,代码块中的代码才会执行。
学会Kotlin的委托功能后,就可以对by lazy的工作原理进行解密了,它的基本语法结构如下:

val p by lazy { ... }

实际上,by lazy并不是连在一起的关键字,只有by才是Kotlin中的关键字,lazy在这里只是一个高阶函数而已。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式,这样表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。
那么话不多说,开始动手编写一个自己的lazy函数

  1. 新建一个Later.kt文件,并编写如下代码:
class Later<T> (val block: () -> T)

这里我们首先定义了一个Later类,并将它指定成泛型类。Later的构造函数中接收一个函数类型参数,这个函数类型参数不接收任何参数,并且返回值类型就是Later类指定的泛型。
2. 接着我们在Later类中实现getValue()方法,代码如下所示:

class Later<T> (val block: () -> T){
var value: Any? = null
operator fun getValue(any: Any?,prop:KProperty<*>) = value?.let {
value as T
}?: block()
}

这里将getValue()方法的第一个参数指定成了Any?类型,表示我们希望Later的委托功能在所有类中都可以使用。然后使用了一个value变量对值进行缓存,如果value为空就调用构造函数中传入的函数类型参数去获取值,否则就直接返回。
由于懒加载技术是不会对属性进行赋值的,因此这里我们就不用实现setValue()方法了。
3. 代码写到这里,委托属性的功能就已经完成了。虽然我们可以立刻使用它,不过为了让它的用法更加类似于lazy函数,最好再定义一个顶层函数。这个函数直接写在Later.kt文件中就可以这个函数直接写在Later.kt文件中就可以了,但是要定义在Later类的外面,因为只有不定义在任何类当中的函数才是顶层函数。代码如下所示:

fun <T> later(block: () -> T) = Later(block)

这个顶层函数的作用很简单:创建Later类的实例,并将接收的函数类型参数传给Later类的构造函数。
4. 接下来测试later方法,代码如下:

fun main() {
val a by later {
listOf("hello", "world")
}
println(a)
// 输出[hello, world]
}
  1. 另外,必须说明的是,虽然我们编写了一个自己的懒加载函数,但由于简单起见,这里只是大致还原了lazy函数的基本实现原理,在一些诸如同步、空值处理等方面并没有实现得很严谨。因此,在正式的项目中,使用Kotlin内置的lazy函数才是最佳的选择。

评论




站点访问量 Loading… 站点访客数 Loading…