ListAdapter не обновляет элемент в RecyclerView: данные меняются, но UI не перерисовывается
Проблема: мутация объектов и неизменяемость списка
Ваш код не работает из-за фундаментальной особенности 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.