[Kotlin] diffUtil을 활용한 RecyclerView 개념 및 사용법 - DiffUtil RecyclerView, Glide RecyclerView에 적용하기 - 뉴스앱 만들기 4편
저번 포스트에서는 RoomDatabase까지 셋업을 해주었습니다. 일부 코드는 그전 포스트들에서 이미 만들었었던 코드들이 있으므로, MVVM의 전체적인 공부를 원하시면 전 포스트 들을 보고 오시는 걸 추천드립니다. DiffUtil만 이해하기 위해 들어오셨어도 예시 코드를 보시는데 어려움이 없으실 겁니다.
DiffUtil을 사용하는 이유
diffUtil을 사용하지 않은 리사이클 러뷰에서는 아래와 같이 recyclerview adapter에 list를 가져오고, 리스트에 아이템 값이 변할 경우 Mainactivity에서 리스트 값에 "notifydataset changed"를 해줘야 했습니다. diffUtil을 사용하면 리스트의 아이템 값이 변경될 때 좀 더 효율적으로 관리할 수 있게 됩니다.
diffUtil 개념
diffUtil은 두 개의 리스트(옛날 버전, 새로운 버전) 가있을 때, 두 리스트에 들어있는 아이템들이 다를 때 새로운 아이템으로 업데이트해줍니다. 그리고 백그라운드에서 계산하기 때문에 main thread를 block하지않고 계산하기때문에 효율적입니다.
이번 예제에서는 옛날 기사의 리스트와 최신 기사의 리스트를 비교하여 업데이트하는 리사이클 러뷰를 만들어보겠습니다.
XML 준비
Glide Dependency 추가하기
기사 이미지는 Glide를 이용하여 넣어주겠습니다.
dependencies {
// Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'
}
Recyclerview Item layout 만들어주기
리사이클 러뷰에 들어갈 item들의 layout입니다.
- 레이아웃 폴더 안에 item_article_preview생성
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<ImageView
android:id="@+id/ivArticleImage"
android:layout_width="160dp"
android:layout_height="90dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvSource"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="SOURCE"
android:textColor="@android:color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ivArticleImage" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="3"
android:text="TITLE"
android:textColor="@android:color/black"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toRightOf="@+id/ivArticleImage"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:ellipsize="end"
android:maxLines="5"
android:text="DESCRIPTION"
android:textColor="@android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/ivArticleImage"
app:layout_constraintTop_toBottomOf="@+id/tvTitle" />
<TextView
android:id="@+id/tvPublishedAt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="PUBLISHED AT"
android:textColor="@android:color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvSource" />
</androidx.constraintlayout.widget.ConstraintLayout>
DiffUtil 만들어주기
파일 만들어주기
RecyclerViewAdapter를 만들어줘야 합니다.
adapter라는 새로운 패키지 아래 NewsAdapter아래에 코틀린 클래스를 만들어주겠습니다.
Recyclerview에 diffUtil 적용하기
DiffUtil은 recyclerAdapter에서 설정해주어야 합니다.
diffUtil 함수는 우리가 만들었던 데이터 클래스인 Article을 타입으로 한 oldItem과 newItem을 비교합니다. oldItem은 업데이트되기 전 리스트고 newItem은 업데이트가 된 리스트입니다.
아래와 같이 DiffUtil 함수를 만들어줍니다.
class NewsAdapter: RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
//viewholder를 만들어줍니다.
inner class ArticleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
private val differCallback = object: DiffUtil.ItemCallback<Article>(){
// 이함수는 oldItem과 new Item이 같은지 확인하는 함수입니다.
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
// oldItem과 newItem은 전에 만들었던 article dataclass의 타입을 가지고있습니다.
// 같은아이템인지 확인하려면 두 아이템에 고유ID를 확인해줘야합니다.
// 기사마자 다른 url을 가지고있기때문에 data class에서 각 아이템의 url을 확인해주겠습니다.
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
//둘의 컨텐츠도 비교해줍니다.
return oldItem == newItem
}
}
}
diffUtil함수를 implement 해주면 2개의 function이 나옵니다.
1. areItemTheSame: 같은 콘텐츠인지 각 아이템들의 고유 ID를 확인하여 체크합니다. 이번 예제에서는 dataclass에 id대신 url를 써주도록 하겠습니다. 왜냐하면 아래 코멘트와 같이 id가 없는 기사들도 있기 때문에 url을 대신 고유 ID로 설정하겠습니다.
2. areContentsTheSame: 콘텐츠가 같은지 확인합니다.
AsyncListdiffer와 RecyclerviewAdapter 함수들 Implement
AsyncListdiffer은 oldList와 newList두개를 가져와서 백그라운드에서 둘이 다른지 계산하는 함수입니다.
- 완성된 newsAdapter 코드
class NewsAdapter: RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
inner class ArticleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
private val differCallback = object: DiffUtil.ItemCallback<Article>(){
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
//추가시작
val differ = AsyncListDiffer(this,differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_article_preview,//위에서 만들었던 item layout을 연결
parent,
false
)
)
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
//각 리스트의 구성요소들을 UI에 연결
holder.itemView.apply{
Glide.with(this).load(article.urlToImage).into(ivArticleImage)
tvSource.text = article.source.name
tvTitle.text = article.title
tvDescription.text = article.description
tvPublishedAt.text = article.publishedAt
각 기사들을 클릭 가능하게 만들어줍니다.
setOnClickListener{
onItemClickListener?.let{
it(article)
}
}
}
}
private var onItemClickListener: ((Article) -> Unit?)? = null
fun setOnItemClickListener(listener: (Article)->Unit){
onItemClickListener = listener
}
override fun getItemCount(): Int {
//리사이클러뷰에서는 보통 리스트를 받아서 그 리스트 사이즈를 count하지만 리스트를 asynclistdiffer가 관리하기때문에, differ에 item갯수로 연결해주어야합니다.
return differ.currentList.size
}
//추가끝
}
diffUtil LiveData의 Observe로 실행시키기
새로운 데이터가 추가되면 Diffutil이 실행되어야합니다. 그러기위해서 MainActivity에 만들어준 어뎁터에 diffUtil을 가져와서 Observe(Live데이터 함수입니다)안에 넣어주어서 라이브 데이터가 바뀔때마다 diffUtil함수가 실행되게합니다.
[ViewModel class이름].[Viewmodel에 데이터를 불러오는 함수].observe(viewLifecycleOwner, Observer {
//변경된 값을 Adapter로 submitList를 통해 전달합니다.
recyclerView.adapter as [어뎁터 class이름]).submitList(it)
})
참고자료