Как изменить состояние CheckBox у одного элемента в RecyclerView без лишних перерисовок

Ответ

Используйте notifyItemChanged(position) для обновления только конкретного элемента. Чтобы избежать мерцания, переопределите getChangePayload() в адаптере:

override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
    if (payloads.isNotEmpty()) {
        // Обновляем только чекбокс
        holder.checkBox.isChecked = items[position].isChecked
    } else {
        super.onBindViewHolder(holder, position, payloads)
    }
}

override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
    return if (oldItem.isChecked != newItem.isChecked) true else null
}

// При изменении состояния:
fun toggleItem(position: Int) {
    items[position].isChecked = !items[position].isChecked
    notifyItemChanged(position, true)
}

Ответ 18+ 🔞

А, ну это же классика, ёпта! Смотри, народ постоянно наступает на одни и те же грабли — обновляют весь список через notifyDataSetChanged(), а потом удивляются, почему всё мерцает, как ёлкина гирлянда. Пиздец, терпения ноль ебать!

Вот смотри, есть же нормальный способ, хитрая жопа. Вместо того чтобы перерисовывать всю хуйню, ты можешь обновить только один конкретный элемент. Для этого есть notifyItemChanged(position). Но если просто вызвать его, система всё равно перерисует всю вьюху элемента, и если там сложная разметка, то глазу будет заметно.

Чтобы этого не было, нужно использовать payloads, блядь. Это как бы намёк системе: "Эй, смотри, тут не всё поменялось, а только одна маленькая деталька, не перегружай всю хуйню".

Вот как это выглядит в коде. Сначала в адаптере переопределяешь метод getChangePayload(). Его задача — сравнить старый и новый элемент и сказать, что именно изменилось. Если изменился только чекбокс, то мы возвращаем какой-нибудь флажок (хоть true), а если всё поменялось — то null.

override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
    return if (oldItem.isChecked != newItem.isChecked) true else null
}

Дальше, нужно перехватить этот payload в onBindViewHolder. Если payload пришёл непустой, значит, обновилось что-то частичное. Тогда мы не вызываем стандартный super.onBindViewHolder, который перерисует всё, а обновляем только то, что нужно — в нашем случае состояние чекбокса.

override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
    if (payloads.isNotEmpty()) {
        // Обновляем только чекбокс
        holder.checkBox.isChecked = items[position].isChecked
    } else {
        super.onBindViewHolder(holder, position, payloads)
    }
}

Ну и когда у тебя меняется состояние, ты вызываешь notifyItemChanged с этим самым payload.

// При изменении состояния:
fun toggleItem(position: Int) {
    items[position].isChecked = !items[position].isChecked
    notifyItemChanged(position, true) // <- вот этот true и есть наш payload
}

Вот и вся магия, ядрёна вошь! Система увидит payload, поймёт, что нужно сделать частичное обновление, и не будет дёргать всю разметку. Мерцание исчезнет, как шампанское на корпоративе. Красота, ебать копать! Главное — не выебывайся и не пытайся через это обновлять текст, картинки и чекбокс одновременно. Для сложных обновлений лучше всё равно делать полный rebind. А для одной кнопочки — в самый раз.