Что такое DiffUtil
Что проверяют
Понимаете ли вы, почему полное обновление адаптера это плохо, и чем платит список за аккуратную перерисовку. За вопросом стоит умение обновлять RecyclerView точечно: сохранять позицию прокрутки, не гасить анимации и не перерисовывать экран целиком из-за одной изменившейся строки.
Как отвечать
DiffUtil считает разницу между старым и новым списком и превращает её во вставки, удаления, перемещения и изменения. Вместо полной перерисовки адаптер получает набор точечных уведомлений.
Нужны два метода: areItemsTheSame отвечает, один и тот же ли это элемент (сравнение по идентификатору), areContentsTheSame отвечает, изменилось ли содержимое (обычно равенство data-класса). Путать их нельзя: первый решает, вставка это или правка, второй решает, перерисовывать ли строку.
Вручную calculateDiff вызывают редко, чаще берут ListAdapter с DiffUtil.ItemCallback: он считает разницу на фоновом потоке и сам применяет результат.
class ItemDiff : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(a: Item, b: Item) = a.id == b.id
override fun areContentsTheSame(a: Item, b: Item) = a == b
}Для тяжёлых строк есть getChangePayload: он позволяет обновить одно поле, не собирая ViewHolder заново.
Где спотыкаются
Меняют тот самый список, который отдали на сравнение. Если старый список чистят и заполняют заново, сравнение идёт списка с самим собой, и обновления теряются. Отдавать нужно неизменяемые копии.
Считают разницу на главном потоке. На длинных списках это заметная пауза, и приходится она ровно на момент прокрутки. Фоновый расчёт даёт ListAdapter.
Сравнивают в areItemsTheSame позиции, а в areContentsTheSame ссылки. В первом случае перемещение выглядит как правка всего списка, во втором каждая новая копия объекта считается изменением и список моргает целиком.