어댑터와 DiffUtil
ListAdapter와 DiffUtil.ItemCallback으로 효율적이고 애니메이션이 적용된 목록 어댑터를 만듭니다. 원활한 부분 업데이트에는 submitList()를 사용합니다.
어댑터와 DiffUtil은(는) CoddyKit의 무료 Android Academy 강의입니다. 이것은 6개 중 6번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Android Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Android Academy 강의에는 총 6개의 강의가 포함되어 있습니다.
notifyDataSetChanged()의 문제점
RecyclerView의 데이터가 변경되었을 때 notifyDataSetChanged()를 호출하면 변경되지 않은 항목까지 모두 다시 그립니다. 이로 인해 다음과 같은 문제가 발생합니다.
- 항목 애니메이션이 없습니다.
- 큰 목록에서 성능이 저하됩니다.
- UI가 깜박입니다.
DiffUtil은 변경 사항을 최소한으로 계산하고 부드러운 애니메이션과 함께 적용합니다.
DiffUtil.ItemCallback
두 항목을 어떻게 비교할지 정의합니다. 다음 두 질문에 답해야 합니다.
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() — 부분 업데이트
좋아요 수처럼 필드 하나만 변경될 때는 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를 빠르게 유지하려면 다음 지침을 따르세요.
- 항목에 고유하고 안정적인 ID가 있다면
setHasStableIds(true)를 사용하세요. onBindViewHolder안에서 객체를 만들지 마세요.RecycledViewPool을 사용하여 여러 RecyclerView에서 재활용된 뷰를 공유하세요.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 AI 튜터), CoddyKit PRO로 업그레이드하면 Android Academy 강의 전체를 잠금 해제할 수 있습니다. Android Academy 강의에는 총 6개의 강의가 포함되어 있습니다.
“어댑터와 DiffUtil”에서 뭘 배우나요?
ListAdapter와 DiffUtil.ItemCallback으로 효율적이고 애니메이션이 적용된 목록 어댑터를 만듭니다. 원활한 부분 업데이트에는 submitList()를 사용합니다. 브라우저에서 직접 실행하는 실습 코드로 Android Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Android Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Android Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 6개 중 6번째 강의입니다.
“어댑터와 DiffUtil” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Android Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Android Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.