ContentProvider学习(三)--ContentObserver和LoadManager(contentprovider是如何实现数据共享的)
概述
经过前两篇笔记的学习,我们已经能够创建内容提供程序,并且知道如何通过内容提供程序对数据进行操作。但是前面的学习都比较粗略,注重于功能的实现,部分实现方式并不推荐,这篇学习笔记中主要是对之前遗漏的一些内容进行补充。
ContentObserver
当我们感兴趣的数据发生变化的时候,我们期望能够及时获得这些变化,从而做出相应的动作,此时我们可以使用ContentObserver
,当相应的数据发生变化的时候,我们能够获得相应的回调。
下面的代码演示了当通讯录里面的数据发生变化的时候,我们重新获取联系人信息的操作。
首先我们需要将ContentObserver
注册到ContentResolver
中,如下代码所示:
//监听联系人数据库的变化 this.contentResolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { super.onChange(selfChange) Log.i(TAG, "onChange: selfChange:$selfChange") } override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) Log.i(TAG, "onChange: selfChange:$selfChange,uri is:$uri") //数据变化后重新获取数据 mHandler.post { mContactAdapter.clear() queryContactList() } } }) 复制代码
在上面的代码中,我们会监听联系人原始数据表的变化,一旦数据有变化,上面重写的这两个方法将会收到回调,通过第二个方法我们可以判断具体发生变化的Uri
,上面的代码并没有对Uri
做判断,当联系人信息有变化的时候,我们会重新读取联系人中的数据。
当我们向联系人数据表中插入数据的时候,我们将会获得以下日志:
I/uri.content_provider.ContactListActivity: onChange: selfChange:false I/uri.content_provider.ContactListActivity: onChange: selfChange:false,uri is:content://com.android.contacts 复制代码
当我们更新一个联系人的数据时候,将会获得以下日志:
I/uri.content_provider.ContactListActivity: onChange: selfChange:false I/uri.content_provider.ContactListActivity: onChange: selfChange:false,uri is:content://com.android.contacts 复制代码
删除一个联系人也会获得以下日志:
I/uri.content_provider.ContactListActivity: onChange: selfChange:false I/uri.content_provider.ContactListActivity: onChange: selfChange:false,uri is:content://com.android.contacts 复制代码
可以看到,通过注册这个回调信息,我们可以在执行完操作以后获得通知,之后就可以执行我们想要的动作了。
CursorLoader
和LoaderManager
上面我们再加载数据的时候很少提及到线程的问题,主要是因为我们的数据较少,很少会遇到性能问题,但即便如此,我们仍然能够从打印日志看到某些时候会有30 ~ 33
帧被跳过,就是因为在主线程中执行了耗时操作。LoadManager
结合CursorLoader
可以帮助我们在子线程加载数据,从而避免在主线程中执行耗时操作。虽然LoaderManager
在高版本已经不推荐使用,官方建议我们迁移到ViewModel
和LiveData
中执行相关的操作,但是下面仍然给出实现方案。
需要说明的是,网上很多案例都是只有一步查询的操作,而且很多都是根据官网中配合ListView
设置的数据,设置数据的时候仅仅是将Cursor
设置进去。而我们的操作则需要执行两步查询才能得出正确的结果,并且第二步查询依赖第一步查询的结果,所以下面给出我的实现方式,感觉这个实现方式并不是很完美,而且在实现过程中遇到了很多问题。
首先需要说明我们的目的是操作通讯录中的联系人信息,包括查询联系人,删除联系人,添加和更新联系人信息。
我们需要明白的是,联系人信息是存放在多个数据表中的,上一步我们监听的ContactsContract.Contacts.CONTENT_URI
这个数据表中的数据我们是不能直接添加或者删除的,我们能够操作的数据表,一个是ContactsContract.RawContacts.CONTENT_URI
这个数据表,这里存储了原始联系人信息,另外一个是ContactsContract.Data.CONTENT_URI
,这里存储了联系人的具体信息,所以我们操作步骤为:
从
ContactsContract.Contacts.CONTENT_URI
表中查询出联系人的原始信息,主要是rawId
根据
rawId
从ContactsContract.Data.CONTENT_URI
表中查询出联系人的具体信息。
需要注意的是:同一个rawId
可以在ContactsContract.Data.CONTENT_URI
查询出多条信息,需要根据MIMETYPE
确定具体的数据类型。
具体操作步骤如下:
首先定义所需的变量常量:
//loadManager private val mLoadManager by lazy { LoaderManager.getInstance(this) } //保存全部的原始联系人列表 private val mRawIdList = mutableListOf<String>() //保存联系人列表数据 private val mContactList = mutableListOf<ContactEntity>() //记录当前获取到第几条数据 private var currentIndex = 0; 复制代码
由于我们需要在页面打开时就获取数据,所以我们在
onCreate()
方法中尝试获取数据
if (mLoadManager.getLoader<Cursor>(0) != null) { mLoadManager.restartLoader(0, null, this) } else { mLoadManager.initLoader(0, null, this) } 复制代码
这里需要说明的是:其实我们一般不会执行到mLoadManager.restartLoader(0, null, this)
,因为当我们执行了mLoadManager.initLoader(0, null, this)
这一步以后,我们的查询操作其实会被缓存起来,当我们不再使用的时候会自动关闭,这里只是做了一个测试。
我们的
Activity
需要实现LoaderManager.LoaderCallbacks<Cursor>
接口,这个接口要求我们实现下面三个方法:
@MainThread @NonNull Loader<D> onCreateLoader(int id, @Nullable Bundle args); @MainThread void onLoadFinished(@NonNull Loader<D> loader, D data); @MainThread void onLoaderReset(@NonNull Loader<D> loader); 复制代码
上面三个方法都是在主线程中执行,其中:
onCreateLoader
是需要我们创建一个CursorLoader
,我们可以根据自己的需要创建对应的CursorLaoder
,其中id
就是我们在上一步mLoadManager.initLoader(0, null, this)
中的0,args
就是上一步中的null.onLoadFinished
当我们创建的CursorLoader
执行完成以后我们将会收到的回调,在这个回调中我们将会获得一个Cursor
,可以用它来获取数据,另外我们无需主动关闭这个Cursor
onLoaderReset
是当我们创建的CursorLoader
被销毁或者被重置的时候会收到的回调。
onCreateLoader
方法的实现:
由于我们需要执行两步查询操作,并且第二步的查询操作依赖第一步的查询结果,所以这里我们首先约定,id为0表示查询原始联系人信息,id为1表示根据rawId
查询联系人详情,所以这个方法的实现如下:
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { Log.i(TAG, "onCreateLoader: TAG,id == $id") if (id == 1) { //此时根据rawId查询具体的联系人信息 if (args == null) { throw IllegalArgumentException("需要数据") } val rawId = args.getString("rawId") return CursorLoader( this, ContactsContract.Data.CONTENT_URI, null, "${ContactsContract.Data.RAW_CONTACT_ID} = ? ", arrayOf(rawId), null ) } //此时id为0,查询全部原始联系人信息 return CursorLoader( this, ContactsContract.Contacts.CONTENT_URI, null, null, null, null ) } 复制代码
在上面的实现中我们根据id创建了不同的查询对象CursorLoader
,需要注意的就是当id == 1
的时候,我们需要从Bundle
中获取数据。
onLoadFinished()
方法的实现
这个方法是获取到数据之后的回调,现在的实现方式如下:
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) { Log.e(TAG, "onLoadFinished: id is:${loader.id} data is:$data") if (loader.id == 0) { data?.let { mRawIdList.clear() mContactList.clear() currentIndex = 0 mContactAdapter.clear() it.moveToPosition(-1) while (it.moveToNext()) { val rawId = it.getString(it.getColumnIndex(ContactsContract.Contacts.NAME_RAW_CONTACT_ID)) Log.i(TAG, "onLoadFinished: rawId is:$rawId") mRawIdList.add(rawId) } //首先获取第一条数据 val firstRawId = mRawIdList[currentIndex] val bundle = Bundle() bundle.putString("rawId", firstRawId) mLoadManager.initLoader(1, bundle, this) } //data?.close() } else if (loader.id == 1) { data?.let { var name = "" var nameId = -1 var phone = "" var phoneId = -1 it.moveToPosition(-1) while (it.moveToNext()) { //获取类型信息 when (it.getString(it.getColumnIndex(ContactsContract.Data.MIMETYPE))) { ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { name = it.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)) nameId = it.getInt(it.getColumnIndex(ContactsContract.Data._ID)) } ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { phone = it.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)) phoneId = it.getInt(it.getColumnIndex(ContactsContract.Data._ID)) } } } mContactList.add( ContactEntity( mRawIdList[currentIndex].toInt(), nameId, phoneId, name, phone ) ) if (currentIndex < mRawIdList.size - 1) { currentIndex++ val rawId = mRawIdList[currentIndex] val bundle = Bundle() bundle.putString("rawId", rawId) mLoadManager.restartLoader(1, bundle, this) } else { Log.i(TAG, "onLoadFinished: complete:${mContactList.size}") mContactAdapter.clear() //更新数据 mContactAdapter.addContact(mContactList) //移除获取详细数据的CursorLoader,防止由于缓存导致这个CursorLoader会收到回调导致后续数据更新不准确 mLoadManager.destroyLoader(1) } } } } 复制代码
上面的逻辑代码如下:
当
id == 0
的时候,此时说明我们查询到了原始联系人信息,这里最终我们需要一个字符串列表,里面保存的是rawId
。由于接下来我们会执行查询联系人详情的操作,所以这里会将最终保存联系人信息的mContactList
重置为空,查询位置currentIndex
重置为0.最重要的一点是
it.moveToPosition(-1)
,前面已经说过,当我们创建一个CursorLoader
之后会将它缓存其它,当数据有变化的时候这里会重新执行查询操作,如果我们不设置这个属性,Cursor
的游标位置会存在于我们上一次遍历的位置,也就是最后的位置。这样会导致后续的操作出现错误。接下来我们会从第一个位置的
rawId
开始,通过mLoadManager.initLoader(1, bundle, this)
创建出查询详细联系人的CursorLoader
,后面接收到第一条数据的时候,我们将currentIndex++
通过restart
方法查询第二条数据,以此类推,直到查询到最后一条数据的时候将查询到的数据设置到RecyclerView
中。当
id == 1
的时候,此时就是查询到的详细联系人信息,我们会获取其中的详细信息并保存下来。当数据请求完成后,我们会把
id = 1
的CursorLoader
销毁掉,因为这个CursorLoader
内部可以接收到数据变化的回调,数据变化后会主动去查询数据,会出现数据不准确的情况。
需要注意的是:上面我们创建了两个CursorLoader
,这两个对象均会被缓存下来,当这两个CursorLoader
中的Uri
对应的数据有变化的时候,均会获得回调从而刷新数据。所以使用LoaderManager
之后我们就不需要在向ContentResolver
注册ContentObserver
了。
作者:ZhangYiFan
链接:https://juejin.cn/post/7028873947655438367