阅读 119

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 复制代码

可以看到,通过注册这个回调信息,我们可以在执行完操作以后获得通知,之后就可以执行我们想要的动作了。

CursorLoaderLoaderManager

上面我们再加载数据的时候很少提及到线程的问题,主要是因为我们的数据较少,很少会遇到性能问题,但即便如此,我们仍然能够从打印日志看到某些时候会有30 ~ 33帧被跳过,就是因为在主线程中执行了耗时操作。LoadManager结合CursorLoader可以帮助我们在子线程加载数据,从而避免在主线程中执行耗时操作。虽然LoaderManager在高版本已经不推荐使用,官方建议我们迁移到ViewModelLiveData中执行相关的操作,但是下面仍然给出实现方案。

需要说明的是,网上很多案例都是只有一步查询的操作,而且很多都是根据官网中配合ListView设置的数据,设置数据的时候仅仅是将Cursor设置进去。而我们的操作则需要执行两步查询才能得出正确的结果,并且第二步查询依赖第一步查询的结果,所以下面给出我的实现方式,感觉这个实现方式并不是很完美,而且在实现过程中遇到了很多问题。

首先需要说明我们的目的是操作通讯录中的联系人信息,包括查询联系人,删除联系人,添加和更新联系人信息。

我们需要明白的是,联系人信息是存放在多个数据表中的,上一步我们监听的ContactsContract.Contacts.CONTENT_URI这个数据表中的数据我们是不能直接添加或者删除的,我们能够操作的数据表,一个是ContactsContract.RawContacts.CONTENT_URI这个数据表,这里存储了原始联系人信息,另外一个是ContactsContract.Data.CONTENT_URI,这里存储了联系人的具体信息,所以我们操作步骤为:

  1. ContactsContract.Contacts.CONTENT_URI表中查询出联系人的原始信息,主要是rawId

  2. 根据rawIdContactsContract.Data.CONTENT_URI表中查询出联系人的具体信息。

需要注意的是:同一个rawId可以在ContactsContract.Data.CONTENT_URI查询出多条信息,需要根据MIMETYPE确定具体的数据类型。

具体操作步骤如下:

  1. 首先定义所需的变量常量:

    //loadManager     private val mLoadManager by lazy {         LoaderManager.getInstance(this)     }     //保存全部的原始联系人列表     private val mRawIdList = mutableListOf<String>()     //保存联系人列表数据     private val mContactList = mutableListOf<ContactEntity>()     //记录当前获取到第几条数据     private var currentIndex = 0; 复制代码

  1. 由于我们需要在页面打开时就获取数据,所以我们在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)这一步以后,我们的查询操作其实会被缓存起来,当我们不再使用的时候会自动关闭,这里只是做了一个测试。

  1. 我们的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被销毁或者被重置的时候会收到的回调。

  1. 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中获取数据。

  1. 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 = 1CursorLoader销毁掉,因为这个CursorLoader内部可以接收到数据变化的回调,数据变化后会主动去查询数据,会出现数据不准确的情况。

需要注意的是:上面我们创建了两个CursorLoader,这两个对象均会被缓存下来,当这两个CursorLoader中的Uri对应的数据有变化的时候,均会获得回调从而刷新数据。所以使用LoaderManager之后我们就不需要在向ContentResolver注册ContentObserver了。


作者:ZhangYiFan
链接:https://juejin.cn/post/7028873947655438367

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐