这是我参与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
可以看到,通过注册这个回调信息,我们可以在执行完操作以后获得通知,之后就可以执行我们想要的动作了。
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,可以用它来获取数据,另外我们无需主动关闭这个CursoronLoaderReset是当我们创建的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了。
本篇学习笔记的源码:查看代码