← Назад к вопросам
Как изменить состояние 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