Reputation: 49
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:
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