Fetching contacts' data into a RecyclerView takes too long time

44 views Asked by At

I am trying to get contacts' data (name, number and photo URI), and it's too slow and showing a black screen for seconds before viewing the UI. When trying to do it in a separate thread, the error below appears to move the process into the main thread. If anyone can help with this, Thank you...

  • android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views
private fun checkPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) !=
            PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), 100)
        } else {
            getContactList()
        }
    }

private fun getContactList() {
        val uri: Uri = ContactsContract.Contacts.CONTENT_URI
        // Sort by ascending
        val sort: String = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME+" ASC"
        // Initialize Cursor
        val cursor: Cursor = this.contentResolver.query(uri, null, null, null, sort)!!
        // Check condition
        if (cursor.count > 0) {
            while (cursor.moveToNext()) {
                val name: String = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.DISPLAY_NAME))
                val img = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI))
                val phoneUri: Uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
                // Initialize phone cursor
                val phoneCursor: Cursor = this.contentResolver.query(phoneUri,
                    null, null, null, sort)!!
                // Check condition
                if (phoneCursor.moveToNext()) {
                    val number: String = phoneCursor.getString(phoneCursor
                        .getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
                    // Initialize Contact Model
                    val model = ContactModel(name, number, img)
                    contactArr.add(model)
                    groupListAdapter.notifyDataSetChanged()
                    // Close Phone Cursor
                    phoneCursor.close()
                }
            }
            // Close Cursor
            cursor.close()
        }
    }
lifecycleScope.launch { checkPermission() }
class ContactModel(var name: String?, var num: String?, var img: String?)

RecyclerView adapter

class GroupsListAdapter(private var groupList: ArrayList<ContactModel>): RecyclerView.Adapter<GroupsListAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.group_list_items_rec, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val currentContact = groupList[position]
        holder.name.text = currentContact.name
        holder.num.text = currentContact.num
        if (currentContact.img != null) {
            Glide.with(holder.itemView.context).load(currentContact.img).into(holder.img)
        } else {
            Glide.with(holder.itemView.context).load(R.drawable.placeholder_p).into(holder.img)
        }
    }

    override fun getItemCount(): Int {
        return groupList.size
    }

    fun createList(addedList: ArrayList<ContactModel>){
        this.groupList = addedList
    }

    class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
        private val binding = GroupListItemsRecBinding.bind(itemView)
        val img: CircleImageView = binding.contactThumbnail
        val name: TextView = binding.contactName
        val num: TextView = binding.contactNum
    }
}
1

There are 1 answers

3
Tenfour04 On

You're trying to update the whole adapter on each item. It would make better sense to collect everything into the list and then update the adapter.

You can run most of your work on the IO dispatcher in case it takes a while to load the contacts, and touch the views using the Main dispatcher. You can switch dispatchers using withContext.

You're also failing to close the cursors under all conditions--you're doing it inside an if statement, so if the condition is false, you are leaking the cursor.

Version of your function with the above problems fixed:

private fun getContactList() = lifecycleScope.launch(Dispatchers.IO) {
    val uri: Uri = ContactsContract.Contacts.CONTENT_URI

    // Sort by ascending
    val sort: String = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME+" ASC"

    val cursor: Cursor = this.contentResolver.query(uri, null, null, null, sort)
        ?.takeIf { it.count > 0) ?: run { 
            Log.w(TAG, "No contracts returned.")
            return@launch
        }

    while (cursor.moveToNext()) {
        val name: String = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.DISPLAY_NAME))
        val img = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI))
        val phoneUri: Uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI

        val phoneCursor: Cursor = this.contentResolver.query(phoneUri, null, null, null, sort)

        if (phoneCursor != null && phoneCursor.moveToNext()) {
            val number: String = phoneCursor.getString(phoneCursor
                    .getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))

            val model = ContactModel(name, number, img)
            contactArr.add(model)
        }
        phoneCursor?.close()
    }

    cursor.close()
    withContext(Dispatchers.Main) { groupListAdapter.notifyDataSetChanged() }
}