Agung kusuma
Agung kusuma

Reputation: 49

notifyItemChanged payload inconsistently triggers when searching and highlighting text in RecyclerView

I'm building a search feature for a chat app where I search for specific text (e.g., 'a') in RecyclerView items and highlight it if found. I use notifyItemChanged(position, payload) to trigger changes only for the relevant items without re-binding the whole view.

The flow is:

  1. A search query is entered (e.g., 'a').
  2. The app searches through the list of Message items, filters the results, and updates isSearched and keyword fields.
  3. For each found item, I call notifyItemChanged(position, arrayListOf(KEY_SEARCHING)), which should trigger the onBindViewHolder with a payload.

The problem: Sometimes the payload works as expected and highlights the text. But in some cases, even though notifyItemChanged is called, the onBindViewHolder with the payload is not triggered, causing the highlight to not appear.

Here’s my current setup:

MessageFragment.kt

    private var currentPosition: Int = -1
    private var searchResults: List<Message> = emptyList()

    private fun searchMessage(searchQuery: String?) {
        messageAdapter?.let { adapter ->
            searchResults = adapter.currentList.filter { item ->
                item.isSearched = true
                item.keyword = searchQuery
                item.content?.contains(searchQuery.toString(), ignoreCase = true) == true
            }

            if (searchResults.isNotEmpty()) {
                currentPosition = searchResults.size - 1

                val position = adapter.currentList.indexOf(searchResults[currentPosition])

                binding.rvMessage.scrollToPosition(position)
                adapter.notifyItemChanged(position, arrayListOf(KEY_SEARCHING))
            } else {
                activity?.toast(getString(R.string.empty_message))
                currentPosition = -1 // Reset position
            }
        }
    }

    private fun searchNextMessage() {
        if (searchResults.isEmpty() || currentPosition == -1) return

        currentPosition++

        if (currentPosition >= searchResults.size) {
            activity?.toast(getString(R.string.empty_message))
            currentPosition = searchResults.size - 1 // Limit position
        } else {
            messageAdapter?.let { adapter ->
                searchResults = adapter.currentList.filter { item ->
                    item.isSearched = true
                    item.keyword = searchQuery
                    item.content?.contains(searchQuery.toString(), ignoreCase = true) == true
                }

                val nextItemPosition = adapter.currentList.indexOf(searchResults[currentPosition])

                binding.rvMessage.scrollToPosition(nextItemPosition)
                adapter.notifyItemChanged(nextItemPosition, arrayListOf(KEY_SEARCHING))
            }
        }
    }

    private fun searchPreviousMessage() {
        if (searchResults.isEmpty() || currentPosition == -1) return

        currentPosition--

        if (currentPosition < 0) {
            activity?.toast(getString(R.string.empty_message))
            currentPosition = 0 // Limit position
        } else {
            messageAdapter?.let { adapter ->
                searchResults = adapter.currentList.filter { item ->
                    item.isSearched = true
                    item.keyword = searchQuery
                    item.content?.contains(searchQuery.toString(), ignoreCase = true) == true
                }

                val previousItemPosition =
                    adapter.currentList.indexOf(searchResults[currentPosition])

                binding.rvMessage.scrollToPosition(previousItemPosition)
                adapter.notifyItemChanged(previousItemPosition, arrayListOf(KEY_SEARCHING))
                }
            }
        }
    }

Message Adapter

   class MessageDiffCallback : DiffUtil.ItemCallback<Message>() {
        override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean {
            return oldItem.id == newItem.id && oldItem.divider == newItem.divider
        }

        override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean {
            return oldItem == newItem
        }

        override fun getChangePayload(oldItem: Message, newItem: Message): Any {
            val data = arrayListOf<String>()
            if (oldItem.displayType != newItem.displayType) {
                data.add(KEY_TYPE)
            }
            if (oldItem.isSearched != newItem.isSearched) {
                data.add(KEY_SEARCHING)
            }
            if (oldItem.keyword != newItem.keyword) {
                data.add(KEY_SEARCHING)
            }
            if (oldItem.isFocused != newItem.isFocused) {
                data.add(KEY_FOCUS)
            }
            return data
        }
    }

override fun onBindViewHolder(
    holder: RecyclerView.ViewHolder,
    position: Int,
    payloads: MutableList<Any>
) {
    if (payloads.isEmpty()) {
        super.onBindViewHolder(holder, position, payloads)
    } else {
        val item = getItem(position)
        val listChanges = payloads.firstOrNull() as? ArrayList<*>
        if (item != null) {
            if (listChanges?.contains(KEY_TYPE) == true) {
                (holder as? BaseViewHolder)?.changeBg(item)
            }
            if (listChanges?.contains(KEY_SEARCHING) == true) {
                (holder as? TextViewHolder)?.bindSearching(item)
            }
            if (listChanges?.contains(KEY_FOCUS) == true) {
                (holder as? TextViewHolder)?.bindFocus(item)
            }
        }
    }

}

    fun bindSearching(item: Message) {
        if (item.isSearched) {
            val sb = SpannableStringBuilder(item.content)
            val word: Pattern = Pattern.compile(item.keyword?.lowercase() ?: "")
            val match: Matcher = word.matcher(item.content?.lowercase() ?: "")

            while (match.find()) {
                itemView.setBackgroundColor(ContextCompat.getColor(itemView.context,R.color.color_backdrop))

                Handler(Looper.getMainLooper()).postDelayed({
                    itemView.setBackgroundColor(ContextCompat.getColor(itemView.context, R.color.white))
                }, 2000)

                val highlightText = BackgroundColorSpan(
                    ContextCompat.getColor(itemView.context, R.color.color_backdrop)
                ) //specify color here
                sb.setSpan(highlightText, match.start(), match.end(), Spannable.SPAN_INCLUSIVE_INCLUSIVE)
            }
            getContentView()?.text = sb
        } else {
            getContentView()?.text = item.content
        }
    }

Upvotes: 0

Views: 31

Answers (0)

Related Questions