0Pricing
Android Academy · Lekcja

Adaptery i DiffUtil

Twórz wydajne, animowane adaptery list za pomocą ListAdapter i DiffUtil.ItemCallback. Używaj submitList() do płynnych, częściowych aktualizacji.

Adaptery i DiffUtil to bezpłatna lekcja Android Academy na CoddyKit. To lekcja 6 z 6. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Android Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Android Academy zawiera 6 lekcji w sumie.

Problem z notifyDataSetChanged()

Gdy dane w RecyclerView ulegają zmianie, wywołanie notifyDataSetChanged() ponownie rysuje każdy element — nawet te, które się nie zmieniły. Powoduje to:

  • brak animacji elementów
  • niską wydajność w przypadku dużych list
  • migotanie interfejsu użytkownika

DiffUtil oblicza minimalny zestaw zmian i stosuje je za pomocą płynnych animacji.

DiffUtil.ItemCallback

Proszę zdefiniować sposób porównywania dwóch elementów. Należy odpowiedzieć na dwa pytania:

  • areItemsTheSame — czy reprezentują ten sam logiczny element? (zwykle porównuje się identyfikatory)
  • areContentsTheSame — czy wszystkie widoczne pola są identyczne?
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 to RecyclerView.Adapter z wbudowanym DiffUtil. Proszę przekazać ItemCallback do jego konstruktora i wywołać submitList(), aby zaktualizować dane:

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"
    }
}

Przesyłanie nowych list

Proszę wywoływać submitList() za każdym razem, gdy dane ulegają zmianie. ListAdapter porównuje starą i nową listę w wątku działającym w tle i stosuje tylko niezbędne zmiany:

// 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)

Obsługa kliknięć w ListAdapter

Proszę przekazywać procedury obsługi kliknięć przez konstruktor — to powszechny i przejrzysty wzorzec:

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() — aktualizacje częściowe

Gdy zmienia się tylko jedno pole (np. liczba polubień), proszę uniknąć pełnego ponownego powiązania za pomocą 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)
    }
}

Wiele typów widoków

Proszę użyć getItemViewType(), aby renderować różne układy elementów na tej samej liście:

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 — łączenie list

ConcatAdapter łączy wiele adapterów w jeden RecyclerView — doskonale nadaje się do nagłówków, stopek i stanów ładowania:

// 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 — ręczne sterowanie

Jeśli nie mogą Państwo rozszerzyć ListAdapter, proszę użyć bezpośrednio 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
}

Wskazówki dotyczące wydajności

Proszę zadbać o szybkość RecyclerView:

  • Proszę użyć setHasStableIds(true), jeśli elementy mają unikalne i stabilne identyfikatory
  • Proszę unikać tworzenia obiektów wewnątrz onBindViewHolder
  • Proszę użyć RecycledViewPool, aby współdzielić widoki z puli recyklingu między wieloma RecyclerView
  • Proszę użyć setItemViewCacheSize(), aby zwiększyć pamięć podręczną elementów znajdujących się poza ekranem

Animacje otrzymują Państwo automatycznie

Integracja ListAdapter z DiffUtil automatycznie uruchamia DefaultItemAnimator dla:

  • pojawiania się nowych elementów przez rozjaśnienie
  • zanikania usuniętych elementów
  • animacji przesunięcia, gdy element zmienia pozycję

Niestandardowy animator można ustawić za pomocą: recyclerView.itemAnimator = MyAnimator(). Ustawienie wartości null wyłącza wszystkie animacje.

Szybkie sprawdzenie

Jakie dwie metody należy zaimplementować w DiffUtil.ItemCallback?

Podsumowanie: adaptery i DiffUtil

Wydajne, animowane listy przy minimalnej ilości kodu:

  • DiffUtil.ItemCallback — definiuje tożsamość (areItemsTheSame) i równość (areContentsTheSame)
  • ListAdapter — asynchroniczne porównywanie list jest wbudowane; należy wywołać submitList()
  • Zawsze należy przekazywać nową referencję listy, nigdy nie modyfikować bieżącej listy
  • getChangePayload() służy do częściowego ponownego powiązania
  • ConcatAdapter służy do łączenia wielu adapterów

Następnie: nawigacja między wieloma ekranami za pomocą Navigation Component.

Często zadawane pytania

Czy lekcja „Adaptery i DiffUtil” jest bezpłatna?

Tak — pełny tekst „Adaptery i DiffUtil” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Android Academy, przejdź na CoddyKit PRO. Kurs Android Academy zawiera 6 lekcji w sumie.

Co nauczysz się w „Adaptery i DiffUtil”?

Twórz wydajne, animowane adaptery list za pomocą ListAdapter i DiffUtil.ItemCallback. Używaj submitList() do płynnych, częściowych aktualizacji. Ćwiczysz Android Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Android Academy?

Nie wymagamy żadnego doświadczenia. Android Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 6 z 6.

Ile czasu zajmuje lekcja „Adaptery i DiffUtil”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Android Academy?

Tak. Każda lekcja Android Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Kolekcje w Kotlinie
  2. Podstawy RecyclerView
  3. Obsługa kliknięć w RecyclerView
  4. SharedPreferences
  5. Preferencje DataStore
  6. Adaptery i DiffUtil
← Powrót do Android Academy