Avatar ·

ListAdapter не обновляет элемент в RecyclerView: данные меняются, но UI не перерисовывается

Всем привет! Я новичок в Kotlin и Android разработке. Использую ListAdapter для RecyclerView, но столкнулся со странной проблемой: когда я меняю данные в списке и вызываю submitList(), элементы не обновляются на экране. Вот мой код: ```kotlin data class Item(val id: Int, var title: String, var isCompleted: Boolean) class ItemDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return oldItem == newItem } } class ItemAdapter : ListAdapter(ItemDiffCallback()) { // ... ViewHolder и bind } // В Activity val items = mutableListOf( Item(1, "Задача 1", false), Item(2, "Задача 2", true) ) adapter.submitList(items) // Пытаюсь обновить элемент items[0].isCompleted = true adapter.submitList(items) // Не работает! ``` Я меняю свойство isCompleted у объекта, передаю тот же список через submitList(), но RecyclerView показывает старые данные. Что я делаю не так? Пробовал вызывать notifyDataSetChanged() — не помогает. Подскажите, как правильно обновлять элементы в ListAdapter? <<>>

Проблема: мутация объектов и неизменяемость списка

Ваш код не работает из-за фундаментальной особенности ListAdapter: он использует DiffUtil для сравнения старого и нового списка. Когда вы изменяете существующий объект Item (мутируете его) и передаёте тот же самый список, DiffUtil видит, что объекты физически те же самые (одинаковые ссылки), и считает, что изменений нет. Даже если вы создаёте новый список, но используете те же объекты, areContentsTheSame вернёт true, потому что объекты равны по ссылке.

Решение: создавайте новые копии объектов

Нужно всегда создавать новые экземпляры данных при изменении. Вот правильный подход:

// Используем data class с копированием (copy)
data class Item(val id: Int, val title: String, val isCompleted: Boolean)

// Вместо мутации создаём новый объект
val updatedItem = items[0].copy(isCompleted = true)
val newList = items.toMutableList().apply {
    this[0] = updatedItem
}
adapter.submitList(newList)

Почему это работает?

  • copy() создаёт новый объект с изменёнными полями
  • toMutableList() создаёт новый список
  • DiffUtil теперь видит, что объекты разные (разные ссылки) и запускает анимацию обновления

Лучшая практика: immutable data и StateFlow

В современном Android-разработке рекомендуется использовать неизменяемые данные и реактивные потоки:

// ViewModel
private val _items = MutableStateFlow<List<Item>>(emptyList())
val items: StateFlow<List<Item>> = _items.asStateFlow()

fun toggleItem(id: Int) {
    _items.update { currentList ->
        currentList.map { item ->
            if (item.id == id) item.copy(isCompleted = !item.isCompleted)
            else item
        }
    }
}

// Activity/Fragment
lifecycleScope.launch {
    viewModel.items.collect { list ->
        adapter.submitList(list)
    }
}

Дополнительные советы:

  • Всегда используйте data class с полями val (не var)
  • Не изменяйте объекты в списке напрямую — создавайте новые
  • Для сложных обновлений используйте submitList с асинхронной передачей (он сам обрабатывает DiffUtil в фоне)
  • Проверьте areContentsTheSame: если сравниваете сложные объекты, убедитесь, что сравниваете все поля

После этих изменений ваш RecyclerView будет корректно обновляться с анимацией!

<<>>

Альтернативное решение: использование ListAdapter с изменяемыми id

Если по каким-то причинам вы не можете использовать copy() (например, у вас сложные объекты с большим количеством полей), можно пойти другим путём — изменить логику areContentsTheSame так, чтобы она сравнивала только ключевые поля, а для определения изменений использовать отдельный флаг версии:

data class Item(
    val id: Int,
    val title: String,
    val isCompleted: Boolean,
    val version: Long = System.currentTimeMillis() // уникальная версия
)

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.version == newItem.version // только по версии!
    }
}

Теперь при каждом изменении вы создаёте новый объект с новой версией, и DiffUtil будет считать их разными. Но этот подход менее надёжен, так как вы теряете точное сравнение содержимого. Рекомендую использовать первый вариант с copy() — он проще и соответствует принципам Kotlin.