0Pricing
Android Academy · Урок

Адаптеры и 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 — локальная установка не требуется.

Все уроки этого курса

  1. Коллекции Kotlin
  2. Основы RecyclerView
  3. События нажатия в RecyclerView
  4. SharedPreferences
  5. Настройки DataStore
  6. Адаптеры и DiffUtil
← Назад к Android Academy