ContentProvider学习(三)--ContentObserver和LoadManager

1,899 阅读3分钟

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

概述

经过前两篇笔记的学习,我们已经能够创建内容提供程序,并且知道如何通过内容提供程序对数据进行操作。但是前面的学习都比较粗略,注重于功能的实现,部分实现方式并不推荐,这篇学习笔记中主要是对之前遗漏的一些内容进行补充。

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了。

本篇学习笔记的源码:查看代码