This is the 8th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
An overview of the
From the previous two notes, we have been able to create content providers and know how to manipulate data through content providers. However, the previous study is rather rough, focusing on the implementation of functions, and some implementation methods are not recommended. This study note is mainly to supplement some content omitted before.
ContentObserver
When the data we are interested in changes, we want to be able to get the changes in time so we can act accordingly, and we can use ContentObserver to get a callback when the data changes.
The following code shows how to retrieve contact information when the data in the address book changes.
First we need to register ContentObserver with the ContentResolver, as shown below:
// Listen for changes in the contact database
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")
// Retrieve data after data changes
mHandler.post {
mContactAdapter.clear()
queryContactList()
}
}
})
Copy the code
In the code above, we will listen to contact the change of the original data table, once the data have change, rewrite the two methods above will receive a callback, by the second method we can determine the specific changes of Uri, the above code does not do to Uri judgment, when the contact information have change, we will read data from a contact again.
When we insert data into the contact table, we get the following log:
I/uri.content_provider.ContactListActivity: onChange: selfChange:false
I/uri.content_provider.ContactListActivity: onChange: selfChange:false,uri is:content://com.android.contacts
Copy the code
When we update a contact’s data, we get the following log:
I/uri.content_provider.ContactListActivity: onChange: selfChange:false
I/uri.content_provider.ContactListActivity: onChange: selfChange:false,uri is:content://com.android.contacts
Copy the code
Deleting a contact will also get the following log:
I/uri.content_provider.ContactListActivity: onChange: selfChange:false
I/uri.content_provider.ContactListActivity: onChange: selfChange:false,uri is:content://com.android.contacts
Copy the code
As you can see, by registering this callback message, we can be notified when the operation is complete, and then we can perform the desired action.
CursorLoader
andLoaderManager
Threads are rarely mentioned when we reload data above, mainly because we have less data and rarely run into performance issues, but even so, we can still see from the print log that at some point 30 ~ 33 frames are skipped because of time-consuming operations performed on the main thread. LoadManager in conjunction with CursorLoader helps us load data in child threads, avoiding time-consuming operations in the main thread. Although LoaderManager is not recommended in older versions, it is recommended that we migrate to ViewModel and LiveData to perform related operations, but the implementation is given below.
It should be noted that many cases on the Internet are one-step query operations, and many of them are based on the official website with the ListView setting data, when setting data is just set Cursor in. However, our operation requires two steps of query to get the correct result, and the second step of query depends on the result of the first step, so my implementation method is given below. I feel that this implementation method is not perfect, and I have encountered many problems in the implementation process.
First of all, we need to explain that our purpose is to operate the contact information in the address book, including query contact, delete contact, add and update contact information.
We need to know is that the contact information is stored in multiple data in the table, on the step we listen ContactsContract. Contacts. CONTENT_URI this data to the data in the table we can’t directly to add or delete, we are able to operate data table, One is ContactsContract. RawContacts. CONTENT_URI this Data table, store the original contact information here, another is ContactsContract. Data. CONTENT_URI, stored here contact details, Therefore, our operation steps are as follows:
- from
ContactsContract.Contacts.CONTENT_URI
The original contact information is displayed in the tablerawId
- According to the
rawId
fromContactsContract.Data.CONTENT_URI
The contact information is displayed in the table.
It is important to note: the same rawId can in ContactsContract. Data. CONTENT_URI query multiple information, need to confirm your specific Data types according to the MIMETYPE.
The specific operation steps are as follows:
- Start by defining the required variable constants:
//loadManager
private val mLoadManager by lazy {
LoaderManager.getInstance(this)}// Save the entire original contact list
private val mRawIdList = mutableListOf<String>()
// Save the contact list data
private val mContactList = mutableListOf<ContactEntity>()
// Record the current number of data
private var currentIndex = 0;
Copy the code
- Since we need to get the data as soon as the page opens, we are in
onCreate()
Method to attempt to retrieve data
if (mLoadManager.getLoader<Cursor>(0) != null) {
mLoadManager.restartLoader(0.null.this)}else {
mLoadManager.initLoader(0.null.this)}Copy the code
Here’s what needs to be said: . In fact, we usually do not perform to mLoadManager restartLoader (0, null, this), because when we performed mLoadManager. InitLoader (0, null, this) after this step, Our queries are actually cached and closed when we don’t need them anymore, just as a test.
- our
Activity
You need to implementLoaderManager.LoaderCallbacks<Cursor>
Interface, which requires us to implement the following three methods:
@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);
Copy the code
The above three methods are executed in the main thread, where:
onCreateLoader
We need to create oneCursorLoader
, we can create corresponding ones according to our own needsCursorLaoder
, includingid
That’s what we did in the previous stepmLoadManager.initLoader(0, null, this)
The zero,args
Null in the previous step.onLoadFinished
When we createCursorLoader
The callback we will receive when the execution is complete, and in this callback we will get oneCursor
We can use it to get data, and we don’t have to actively turn this offCursor
onLoaderReset
Is when we createCursorLoader
The callback received when destroyed or reset.
onCreateLoader
Method implementation:
Since we need to perform two-step query operation, and the query operation of the second step depends on the query result of the first step, here we first agree that ID 0 means to query the original contact information, id 1 means to query the contact details according to rawId, so the implementation of this method is as follows:
override fun onCreateLoader(id: Int, args: Bundle?).: Loader<Cursor> {
Log.i(TAG, "onCreateLoader: TAG,id == $id")
if (id == 1) {
// Query specific contact information according to rawId
if (args == null) {
throw IllegalArgumentException("Need data")}val rawId = args.getString("rawId")
return CursorLoader(
this, ContactsContract.Data.CONTENT_URI,
null."${ContactsContract.Data.RAW_CONTACT_ID}=? ",
arrayOf(rawId),
null)}// If the id is 0, query all original contact information
return CursorLoader(
this, ContactsContract.Contacts.CONTENT_URI,
null.null.null.null)}Copy the code
In the above implementation we create a different query object CursorLoader based on id. Note that when id == 1, we need to fetch data from the Bundle.
onLoadFinished()
Method implementation
This method is a callback after the data is retrieved, and is now implemented as follows:
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)
}
// Get the first data
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()) {
// Get the type information
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()
// Update data
mContactAdapter.addContact(mContactList)
// Remove the CursorLoader that fetched the detailed data to prevent the CursorLoader from receiving a callback due to caching, resulting in inaccurate subsequent data updates
mLoadManager.destroyLoader(1)}}}}Copy the code
The above logical code is as follows:
- when
id == 0
“, this means we have queried the original contact information, here we finally need a list of strings, which store israwId
. The contact information will eventually be saved here as we are going to query the contact detailsmContactList
Reset to null, query the locationcurrentIndex
Reset to zero. - The most important thing is this
it.moveToPosition(-1)
As mentioned earlier, when we create aCursorLoader
We’re going to cache it later, and then we’re going to redo the query when the data changes, and if we don’t set this property,Cursor
The cursor position of will exist in the position we walked through last time, which is the last position. This may cause errors in subsequent operations. - And then we’re going to go from the first position
rawId
Start, passmLoadManager.initLoader(1, bundle, this)
Created to query detailed contactsCursorLoader
After receiving the first data, we willcurrentIndex++
throughrestart
Method to query the second data, and so on, until the query to the last data set toRecyclerView
In the. - when
id == 1
“, this is the detailed contact information queried, we will obtain the detailed information and save it. - When the data request is complete, we will put
id = 1
theCursorLoader
Destroy it because of thisCursorLoader
The internal system can receive the callback of data changes, and will take the initiative to query data after data changes, resulting in inaccurate data.
Note that we have created two cursorLoaders. These two objects are cached. When the Uri of these two CursorLoaders changes, the data will be refreshed by callback. So with LoaderManager we no longer need to register ContentObserver with ContentResolver.
View the code