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
在得到了内容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->{ } table1Item->{ } table2Dir->{ } table2Item->{ } } } }
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.providertestimport android.annotation .SuppressLintimport android.net.Uriimport android.os.Bundleimport android.util.Logimport androidx.activity.enableEdgeToEdgeimport androidx.appcompat.app.AppCompatActivityimport androidx.core.content.contentValuesOfimport androidx.core.view.ViewCompatimport androidx.core.view.WindowInsetsCompatimport com.example.providertest.databinding.ActivityMainBindingclass 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的工作原理进行解密了,它的基本 语法结构如下:
现在再来看这段代码,是不是觉得更有头绪了呢?实际上,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()方法了。