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.

CursorLoaderandLoaderManager

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:

  1. fromContactsContract.Contacts.CONTENT_URIThe original contact information is displayed in the tablerawId
  2. According to therawIdfromContactsContract.Data.CONTENT_URIThe 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:

  1. 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
  1. Since we need to get the data as soon as the page opens, we are inonCreate()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.

  1. ourActivityYou 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:

  • onCreateLoaderWe need to create oneCursorLoader, we can create corresponding ones according to our own needsCursorLaoder, includingidThat’s what we did in the previous stepmLoadManager.initLoader(0, null, this)The zero,argsNull in the previous step.
  • onLoadFinishedWhen we createCursorLoaderThe callback we will receive when the execution is complete, and in this callback we will get oneCursorWe can use it to get data, and we don’t have to actively turn this offCursor
  • onLoaderResetIs when we createCursorLoaderThe callback received when destroyed or reset.
  1. onCreateLoaderMethod 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.

  1. 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:

  • whenid == 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 detailsmContactListReset to null, query the locationcurrentIndexReset to zero.
  • The most important thing is thisit.moveToPosition(-1)As mentioned earlier, when we create aCursorLoaderWe’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,CursorThe 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 positionrawIdStart, passmLoadManager.initLoader(1, bundle, this)Created to query detailed contactsCursorLoaderAfter receiving the first data, we willcurrentIndex++throughrestartMethod to query the second data, and so on, until the query to the last data set toRecyclerViewIn the.
  • whenid == 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 putid = 1theCursorLoaderDestroy it because of thisCursorLoaderThe 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