0Pricing
Android Academy · レッスン

AdaptersとDiffUtil

ListAdapterとDiffUtil.ItemCallbackを使って、効率的でアニメーション付きのリストアダプターを構築します。submitList()で滑らかな部分更新を実現します。

「AdaptersとDiffUtil」はCoddyKit上の無料Android Academyレッスンです。 これはレッスン6/6です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAndroid Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Android Academyコースには全6レッスンが含まれています。

notifyDataSetChanged()の問題点

RecyclerViewのデータが変わったときにnotifyDataSetChanged()を呼び出すと、変更されていないものも含め、すべてのアイテムが再描画されます。これにより、次の問題が発生します。

  • アイテムのアニメーションがない
  • 大きなリストでパフォーマンスが低下する
  • UIがちらつく

DiffUtilは変更の最小限の集合を計算し、滑らかなアニメーションとともに適用します。

DiffUtil.ItemCallback

2つのアイテムをどのように比較するかを定義します。次の2つの問いに答えます。

  • areItemsTheSame — 同じ論理上のアイテムを表していますか?(通常はIDを比較します)
  • 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はDiffUtilを組み込んだRecyclerView.Adapterです。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() — 部分更新

いいね数など、1つのフィールドだけが変更された場合は、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)
    }
}

複数のViewタイプ

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は複数のアダプターを1つの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を高速に保ちます。

  • アイテムに一意で安定したIDがある場合は、setHasStableIds(true)を使います
  • onBindViewHolder内でオブジェクトを作成しないでください
  • RecycledViewPoolを使い、複数のRecyclerView間で再利用ビューを共有します
  • setItemViewCacheSize()を使って画面外キャッシュを増やします

アニメーションは自動的に適用される

ListAdapterとDiffUtilの統合により、次の処理に対してDefaultItemAnimatorが自動的に実行されます。

  • 新しいアイテムのフェードイン
  • 削除されたアイテムのフェードアウト
  • アイテムの位置が変わったときの移動アニメーション

カスタムアニメーターを設定するには、recyclerView.itemAnimator = MyAnimator()とします。nullを設定すると、すべてのアニメーションが無効になります。

理解度チェック

DiffUtil.ItemCallbackで実装する必要がある2つのメソッドは何ですか?

まとめ:アダプターとDiffUtil

最小限のコードで、効率的かつアニメーション付きのリストを実現できます。

  • DiffUtil.ItemCallback — 同一性(areItemsTheSame)と等価性(areContentsTheSame)を定義する
  • ListAdapter — 非同期の差分計算を組み込みで提供し、submitList()を呼び出す
  • 現在のリストを変更せず、必ず新しいリストの参照を送信する
  • 部分的な再バインドにはgetChangePayload()を使う
  • 複数のアダプターを組み合わせるにはConcatAdapterを使う

次は、Navigation Componentを使った複数画面のナビゲーションです。

よくある質問

「AdaptersとDiffUtil」レッスンは無料ですか?

はい。「AdaptersとDiffUtil」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Android Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Android Academyコースには全6レッスンが含まれています。

「AdaptersとDiffUtil」で何を学びますか?

ListAdapterとDiffUtil.ItemCallbackを使って、効率的でアニメーション付きのリストアダプターを構築します。submitList()で滑らかな部分更新を実現します。 ブラウザで直接実行するハンズオンコードでAndroid Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Android Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAndroid Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン6/6です。

「AdaptersとDiffUtil」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAndroid Academyレッスンでコードを書いて実行できますか?

はい。すべてのAndroid Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Kotlinコレクション
  2. RecyclerViewの基礎
  3. RecyclerViewのクリックイベント
  4. SharedPreferences
  5. DataStore Preferences
  6. AdaptersとDiffUtil
← Android Academyに戻る