← Назад к вопросам

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

2.0 Middle🔥 181 комментариев
#UI и вёрстка#Производительность и оптимизация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Этофонтипичная задача — обновить один элемент без полной перерисовки всего списка.

1. Неправильный способ (полная перерисовка)

// ПЛОХО: перерисует ВСЕ элементы
checkBox.setOnCheckedChangeListener { _, isChecked ->
    data[position].isChecked = isChecked
    adapter.notifyDataSetChanged()  // Перерисует всё!
}

Проблема:

  • Теряется скролл позиция
  • Мерцание всего списка
  • Плохая производительность

2. Правильный способ: notifyItemChanged

// ХОРОШО: перерисует только один элемент
class MyAdapter(private val data: List<Item>) : RecyclerView.Adapter<MyViewHolder>() {
    
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = data[position]
        
        holder.checkBox.setOnCheckedChangeListener(null)  // Отключить слушатель
        holder.checkBox.isChecked = item.isChecked
        
        // Включить слушатель с правильным position
        holder.checkBox.setOnCheckedChangeListener { _, isChecked ->
            data[position].isChecked = isChecked
            notifyItemChanged(position)  // Только этот элемент!
        }
    }
    
    override fun getItemCount(): Int = data.size
}

3. Оптимальный способ: Payload

Payload позволяет обновить только нужные поля без полной перерисовки:

class MyAdapter(private val data: List<Item>) : RecyclerView.Adapter<MyViewHolder>() {
    
    override fun onBindViewHolder(
        holder: MyViewHolder,
        position: Int,
        payloads: List<Any>
    ) {
        if (payloads.isEmpty()) {
            // Полная привязка
            onBindViewHolder(holder, position)
        } else {
            // Частичное обновление (только CheckBox)
            val payload = payloads[0] as? String
            if (payload == "checked") {
                holder.checkBox.isChecked = data[position].isChecked
            }
        }
    }
    
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = data[position]
        
        holder.title.text = item.title
        holder.description.text = item.description
        
        holder.checkBox.setOnCheckedChangeListener(null)
        holder.checkBox.isChecked = item.isChecked
        
        holder.checkBox.setOnCheckedChangeListener { _, isChecked ->
            data[position].isChecked = isChecked
            // Отправить payload вместо обновления
            notifyItemChanged(position, "checked")
        }
    }
    
    override fun getItemCount(): Int = data.size
}

4. С DiffUtil для больших списков

class ItemDiffCallback : DiffUtil.ItemCallback<Item>() {
    override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
        return oldItem.id == newItem.id
    }
    
    override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
        return oldItem == newItem
    }
    
    override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
        // Вернуть payload если только checked изменился
        if (oldItem.isChecked != newItem.isChecked) {
            return "checked"
        }
        return null
    }
}

class MyAdapter : ListAdapter<Item, MyViewHolder>(ItemDiffCallback()) {
    
    override fun onBindViewHolder(
        holder: MyViewHolder,
        position: Int,
        payloads: List<Any>
    ) {
        if (payloads.isNotEmpty() && payloads[0] == "checked") {
            // Обновить только CheckBox
            holder.checkBox.isChecked = getItem(position).isChecked
        } else {
            onBindViewHolder(holder, position)
        }
    }
    
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = getItem(position)
        
        holder.title.text = item.title
        holder.checkBox.setOnCheckedChangeListener(null)
        holder.checkBox.isChecked = item.isChecked
        
        holder.checkBox.setOnCheckedChangeListener { _, isChecked ->
            // Обновить данные
            val newItem = item.copy(isChecked = isChecked)
            submitList(currentList.toMutableList().apply {
                set(position, newItem)
            })
        }
    }
}

5. Проблема: listener срабатывает при bind

// ПРОБЛЕМА: listener вызывается во время bind!
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val checkBox: CheckBox = itemView.findViewById(R.id.checkbox)
    
    fun bind(item: Item) {
        // Если здесь установить listener, он сработает
        // когда setChecked() вызовется в следующей строке!
        checkBox.setOnCheckedChangeListener { _, isChecked ->
            item.isChecked = isChecked
        }
        checkBox.isChecked = item.isChecked  // Сработает listener!
    }
}

Решение: отключить listener перед bind

fun bind(item: Item, onCheckedChange: (Int, Boolean) -> Unit) {
    checkBox.setOnCheckedChangeListener(null)  // Отключить!
    checkBox.isChecked = item.isChecked       // Теперь listener не сработает
    
    checkBox.setOnCheckedChangeListener { _, isChecked ->
        onCheckedChange(bindingAdapterPosition, isChecked)
    }
}

6. Полный пример: Todo List с CheckBox

data class Todo(val id: Int, val title: String, var isCompleted: Boolean = false)

class TodoAdapter(
    private val todos: MutableList<Todo>,
    private val onToggle: (Int, Boolean) -> Unit
) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
        val itemView = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_todo, parent, false)
        return TodoViewHolder(itemView)
    }
    
    override fun onBindViewHolder(
        holder: TodoViewHolder,
        position: Int,
        payloads: List<Any>
    ) {
        if (payloads.isNotEmpty()) {
            // Только CheckBox
            holder.bindCheckBox(todos[position])
        } else {
            // Полная привязка
            onBindViewHolder(holder, position)
        }
    }
    
    override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
        holder.bind(todos[position], position)
    }
    
    override fun getItemCount() = todos.size
    
    inner class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val checkBox: CheckBox = itemView.findViewById(R.id.checkbox)
        private val title: TextView = itemView.findViewById(R.id.title)
        
        fun bind(todo: Todo, position: Int) {
            title.text = todo.title
            title.paintFlags = if (todo.isCompleted) {
                title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
            } else {
                title.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
            }
            
            bindCheckBox(todo)
            
            checkBox.setOnCheckedChangeListener { _, isChecked ->
                todo.isCompleted = isChecked
                onToggle(position, isChecked)
                notifyItemChanged(position, "checked")  // Payload!
            }
        }
        
        fun bindCheckBox(todo: Todo) {
            checkBox.setOnCheckedChangeListener(null)  // Отключить listener
            checkBox.isChecked = todo.isCompleted
        }
    }
}

7. Использование

val adapter = TodoAdapter(todoList) { position, isChecked ->
    // Отправить на сервер
    api.updateTodo(todoList[position].id, isChecked)
}

recyclerView.adapter = adapter

8. Таблица: способы обновления

МетодПерерисовкаСкоростьИспользуй когда
notifyDataSetChanged()ВСЕМедленноРедко (полная замена)
notifyItemChanged(pos)ОдинБыстроОбновление элемента
notifyItemChanged(pos, payload)Один, частичноОчень быстроОбновление поля
DiffUtilМинимальноСамая быстраяСписки сanimatin

9. Избегай этих ошибок

❌ Ошибка 1: setOnCheckedChangeListener внутри bind

// Плохо: listener вызывается при setChecked()
checkBox.setOnCheckedChangeListener { _, isChecked ->
    // Сработает здесь
}
checkBox.isChecked = true  // TRIGGER!

❌ Ошибка 2: использовать position из слушателя

// Плохо: position может измениться при удалении элемента выше
checkBox.setOnCheckedChangeListener { _, isChecked ->
    data[position].isChecked = isChecked  // position может быть неправильным!
}

✅ Правильно: использовать bindingAdapterPosition

checkBox.setOnCheckedChangeListener { _, isChecked ->
    data[bindingAdapterPosition].isChecked = isChecked
    notifyItemChanged(bindingAdapterPosition, "checked")
}

Итог

  • notifyDataSetChanged() = убить производительность
  • notifyItemChanged(position) = обновить один элемент
  • notifyItemChanged(position, payload) = обновить только нужные поля
  • DiffUtil = идеально для списков с анимацией
  • Всегда отключай listener перед bind
  • Используй bindingAdapterPosition вместо position
Как изменить состояние CheckBox у одного элемента в RecyclerView без лишних перерисовок | PrepBro