Адаптеры и DiffUtil
Создавайте эффективные анимированные адаптеры списков с помощью ListAdapter и DiffUtil.ItemCallback. Используйте submitList() для плавного частичного обновления.
«Адаптеры и DiffUtil» — бесплатный урок Android Academy на CoddyKit. Это урок 6 из 6. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Android Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Android Academy содержит 6 уроков всего.
Проблема с notifyDataSetChanged()
Когда данные в RecyclerView изменяются, вызов notifyDataSetChanged() перерисовывает каждый элемент, даже неизменившиеся. Это приводит к следующим проблемам:
- Отсутствие анимации элементов
- Низкая производительность в больших списках
- Мерцание интерфейса
DiffUtil вычисляет минимальный набор изменений и применяет его с плавной анимацией.
DiffUtil.ItemCallback
Определите, как сравнивать два элемента. Нужно ответить на два вопроса:
areItemsTheSame— представляют ли они один и тот же логический элемент? (обычно сравниваются идентификаторы)areContentsTheSame— совпадают ли все отображаемые поля?
data class Post(val id: Int, val title: String, val likes: Int)
class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem.id == newItem.id // same post?
}
override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem == newItem // data class equality (all fields)
}
}ListAdapter
ListAdapter — это RecyclerView.Adapter со встроенным DiffUtil. Передайте ему ItemCallback в конструкторе и вызывайте submitList() для обновления данных:
class PostAdapter : ListAdapter<Post, PostAdapter.ViewHolder>(PostDiffCallback()) {
inner class ViewHolder(val binding: ItemPostBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = getItem(position) // use getItem(), not a manual list
holder.binding.tvTitle.text = post.title
holder.binding.tvLikes.text = "${post.likes} likes"
}
}Передача новых списков
Вызывайте submitList() при каждом изменении данных. ListAdapter сравнивает старый и новый списки в фоновом потоке и применяет только необходимые изменения:
// In Activity or Fragment:
val adapter = PostAdapter()
binding.recyclerView.adapter = adapter
// Observe ViewModel LiveData:
viewModel.posts.observe(viewLifecycleOwner) { posts ->
adapter.submitList(posts) // diff calculated off main thread
}
// Important: always submit a NEW list, not a mutated old one
// WRONG: currentList.add(newPost); adapter.submitList(currentList)
// RIGHT: adapter.submitList(currentList + newPost)Обработчики нажатий в ListAdapter
Передавайте обработчики нажатий через конструктор — это распространённый и удобный подход:
class PostAdapter(
private val onLike: (Post) -> Unit,
private val onOpen: (Post) -> Unit
) : ListAdapter<Post, PostAdapter.ViewHolder>(PostDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = getItem(position)
holder.binding.tvTitle.text = post.title
holder.binding.btnLike.setOnClickListener { onLike(post) }
holder.itemView.setOnClickListener { onOpen(post) }
}
}
// Usage:
val adapter = PostAdapter(
onLike = { viewModel.likePost(it) },
onOpen = { navigateToDetail(it.id) }
)getChangePayload() — частичные обновления
Если изменилось только одно поле, например количество отметок «Нравится», избегайте полной повторной привязки с помощью getChangePayload():
class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Post, newItem: Post) = oldItem == newItem
override fun getChangePayload(oldItem: Post, newItem: Post): Any? {
// Return non-null payload to trigger partial bind
return if (oldItem.likes != newItem.likes) "likes_changed" else null
}
}
// In Adapter.onBindViewHolder with payloads:
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List<Any>) {
if (payloads.contains("likes_changed")) {
holder.binding.tvLikes.text = "${getItem(position).likes} likes"
} else {
super.onBindViewHolder(holder, position, payloads)
}
}Несколько типов представлений
Используйте getItemViewType(), чтобы отображать в одном списке элементы с разными макетами:
class FeedAdapter : ListAdapter<FeedItem, RecyclerView.ViewHolder>(FeedDiffCallback()) {
companion object {
const val TYPE_POST = 0
const val TYPE_AD = 1
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is FeedItem.Post -> TYPE_POST
is FeedItem.Ad -> TYPE_AD
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
TYPE_POST -> PostViewHolder(ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false))
else -> AdViewHolder(ItemAdBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
}ConcatAdapter — объединение списков
ConcatAdapter объединяет несколько адаптеров в один RecyclerView — это удобно для заголовков, нижних колонтитулов и состояний загрузки:
// Separate adapters for different sections
val headerAdapter = HeaderAdapter()
val postAdapter = PostAdapter()
val loaderAdapter = LoadingAdapter()
// Combine them in order
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, postAdapter, loaderAdapter)
// Update each independently
postAdapter.submitList(posts)
loaderAdapter.showLoading(true)AsyncListDiffer — ручное управление
Если вы не можете расширить ListAdapter, используйте AsyncListDiffer напрямую:
class CustomAdapter : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
private val differ = AsyncListDiffer(this, PostDiffCallback())
fun submitList(list: List<Post>) = differ.submitList(list)
fun getItem(position: Int): Post = differ.currentList[position]
override fun getItemCount() = differ.currentList.size
}Советы по производительности
Поддерживайте высокую скорость работы RecyclerView:
- Используйте
setHasStableIds(true), если у элементов есть уникальные стабильные идентификаторы - Не создавайте объекты внутри
onBindViewHolder - Используйте
RecycledViewPool, чтобы совместно использовать переработанные представления в нескольких RecyclerViews - Используйте
setItemViewCacheSize(), чтобы увеличить кэш представлений за пределами экрана
Анимации без дополнительных усилий
Интеграция ListAdapter с DiffUtil автоматически запускает DefaultItemAnimator для следующих случаев:
- Плавное появление новых элементов
- Плавное исчезновение удалённых элементов
- Анимация перемещения при изменении позиции элемента
Установите собственный аниматор: recyclerView.itemAnimator = MyAnimator(). Установите значение null, чтобы отключить все анимации.
Быстрая проверка
Какие два метода необходимо реализовать в DiffUtil.ItemCallback?
Итоги: адаптеры и DiffUtil
Эффективные списки с анимацией и минимальным количеством кода:
DiffUtil.ItemCallback— определяет идентичность (areItemsTheSame) и равенство (areContentsTheSame)ListAdapter— асинхронное сравнение списков встроено; вызывайтеsubmitList()- Всегда передавайте ссылку на новый список, никогда не изменяйте текущий список
getChangePayload()— для частичной повторной привязкиConcatAdapter— для объединения нескольких адаптеров
Далее: навигация между несколькими экранами с помощью Navigation Component.
Часто задаваемые вопросы
Урок «Адаптеры и DiffUtil» бесплатный?
Да — полный текст урока «Адаптеры и DiffUtil» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Android Academy, подпишись на CoddyKit PRO. Курс Android Academy содержит 6 уроков всего.
Чему я научусь в уроке «Адаптеры и DiffUtil»?
Создавайте эффективные анимированные адаптеры списков с помощью ListAdapter и DiffUtil.ItemCallback. Используйте submitList() для плавного частичного обновления. Ты практикуешь Android Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Android Academy?
Предыдущий опыт не требуется. Android Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 6 из 6.
Сколько времени занимает урок «Адаптеры и DiffUtil»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Android Academy?
Да. Каждый урок Android Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Коллекции Kotlin
- Основы RecyclerView
- События нажатия в RecyclerView
- SharedPreferences
- Настройки DataStore
- Адаптеры и DiffUtil