관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] RecyclerView Diffutil, ListAdapter예제 정리 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] RecyclerView Diffutil, ListAdapter예제 정리

막무가내막내 2021. 1. 10. 12:13
728x90

 

[2021-04-27 업데이트]

[개념(출처) 참고 및 공부자료들]

thdev.tech/kotlin/2020/09/22/kotlin_effective_03/

 

data class를 활용하여 RecyclerView.DiffUtil을 잘 활용하는 방법 |

I’m an Android Developer.

thdev.tech

velog.io/@l2hyunwoo/Android-RecyclerView-DiffUtil-ListAdapter

 

[Kotlin Android] RecyclerView 어댑터의 데이터 빠르게 바꾸기 - ListAdapter와 DiffUtil 사용하기

RecyclerView 2탄! 빠른 데이터 교체를 원한다면 DiffUtil을 주저하지 말고 사용하세요

velog.io

developer.android.com/codelabs/kotlin-android-training-diffutil-databinding#0

 

Android Kotlin Fundamentals: DiffUtil and data binding with RecyclerView

Learn techniques that make RecyclerView more efficient for large lists. Also learn techniques to make your code easier to maintain and extend for complex lists and grids in your Android Kotlin apps.

developer.android.com

developer.android.com/reference/androidx/recyclerview/widget/ListAdapter

 

ListAdapter  |  Android 개발자  |  Android Developers

ListAdapter public abstract class ListAdapter extends Adapter RecyclerView.Adapter base class for presenting List data in a RecyclerView, including computing diffs between Lists on a background thread. This class is a convenience wrapper around AsyncListDi

developer.android.com

 

기본내용과 원리는 위 참고 사이트들에 너무나도 잘 나와있어 저는 간략히 정리만 조금 하겠습니다. 제가 쓰는 것보다 저분들의 포스팅을 한번더 보는게 이득입니다. ㅋㅋㅋ

[DiffUtil 이란?]

요약하면 안드로이드 어댑터에서 현재 데이터 리스트와 교체될 데이터 리스트를 비교하여 무엇이 다른지(바꼈는지) 알아내는 클래스로 리사이클러뷰에서 기존 아이템리스트에 수정 혹은 변경이 있을 시 전체를 갈아치우는게 아니라 변경되야하는 데이터만 빠르게 바꿔주는 역할을 합니다.

 

[함수 정리]

  • getOldListSize : 현재 리스트에 노출하고 있는 List size
  • getNewListSize : 새로 추가하거나, 갱신해야 할 List size
  • areItemsTheSame : 현재 리스트에 노출하고 있는 아이템과 새로운 아이템이 서로 같은지 비교한다. 보통 고유한 ID 값을 체크한다.
  • areContentsTheSame : 현재 리스트에 노출하고 있는 아이템과 새로운 아이템의 equals를 비교한다.

 

 

[ListAdapter 란?]

 

ListAdapter는  안드로이드 문서 그대로 번역하면 RecyclerView.Adapter 를 베이스로 한 클래스로 RecyclerView 의 

List 데이터를 표현해주며 List를 백그라운드 스레드에서 diff(차이)를 처리하는 특징이 있습니다.

 

이 클래스는 AsyncListDiffer 아이템 접근 및 카운팅에 대한 어댑터 공통 기본 동작을 구현 하는 편리한 Wrapper 입니다.

LiveData <List>를 사용하면 어댑터에 데이터를 쉽게 제공 할 수 있지만 필수는 아닙니다 submitList(List). 새 목록을 사용할 수있을 때 사용할 수 있습니다. 

 

 

 

그래도 ListAdapter의 기본적인 메소드만 몇개 살펴보겠습니다. 

 

getItem(position: Int) : protected method로 클래스 내부에서 사용하며 어댑터 내 아이템 List 를 인덱싱할때 사용합니다. 기존 val items : List<User>식으로 어댑터내에서 선언해서 items[position] 식으로 사용했는데 ListAdapter + Diffutil에서는 아이템 리스트도 알아서 관리해주기 때문에 기본적으로 리스트 선언이 필요없고 이 리스트의 아이템을 가져오는데 사용(대체)됩니다. 

 

getCurrentList() : 어댑터가 가지고 있는 리스트를 가져올 때 사용합니다.

 

submitList(List<T> list): 리스트 항목을 변경하고 싶을 때 사용합니다 기존 일반 어댑터의 add(), notifyDataSetChanged()를 대체한다고 보면 됩니다.

 

 

 

 

 


제가 짠 예제 코드입니다.

 

[ListAdapter]

데이터바인딩 적용

package com.mtjin.cnunoticeapp.views.employ

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.cnunoticeapp.R
import com.mtjin.cnunoticeapp.data.employ.EmployNotice
import com.mtjin.cnunoticeapp.databinding.ItemEmployBinding

class EmployAdapter(
    private val itemClick: (EmployNotice) -> Unit,
    private val numClick: (EmployNotice) -> Unit
) : ListAdapter<EmployNotice, EmployAdapter.ViewHolder>(
    diffUtil
) {
    //리스트 선언 필요X
    //private val items = mutableListOf<EmployNotice>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding: ItemEmployBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_employ,
            parent,
            false
        )
        val viewHolder = ViewHolder(binding)
        binding.apply {
            root.setOnClickListener {
                itemClick(getItem(viewHolder.adapterPosition)) //getItem()으로 아이템 가져옴
            }
            employTvNum.setOnClickListener {
                numClick(getItem(viewHolder.adapterPosition))
            }
        }
        return viewHolder
    }

    //getItemCount() 오버라이딩 메서드 사라짐
    //override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position)) //변경된 점 -> getItem(position) 메서드가 생겼다.
    }

    class ViewHolder(private val binding: ItemEmployBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(item: EmployNotice) {
            binding.item = item
            binding.executePendingBindings()
        }
    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<EmployNotice>() {
            override fun areContentsTheSame(oldItem: EmployNotice, newItem: EmployNotice) =
                oldItem == newItem

            override fun areItemsTheSame(oldItem: EmployNotice, newItem: EmployNotice) =
                oldItem.link == newItem.link
        }
    }
}

 


 

[ViewModel]

그냥 가져오는거와 페이징만큼 가져오는 함수 2개가 존재합니다. 가져온 데이터는 라이브데이터 리스트에 세팅되고 이 리스트는 xml에 데이터바인딩 되어있습니다.

package com.mtjin.cnunoticeapp.views.employ

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.mtjin.cnunoticeapp.base.BaseViewModel
import com.mtjin.cnunoticeapp.data.employ.EmployNotice
import com.mtjin.cnunoticeapp.data.employ.source.EmployNoticeRepository
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers

class EmployNoticeViewModel(private val repository: EmployNoticeRepository) :
    BaseViewModel() {
    private val _noticeList = MutableLiveData<MutableList<EmployNotice>>()

    val noticeList: LiveData<MutableList<EmployNotice>> get() = _noticeList

    fun requestNotice() {
        compositeDisposable.add(
            repository.requestNotice()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { showProgress() }
                .doAfterTerminate { hideProgress() }
                .subscribe({
                    _noticeList.value = it as MutableList<EmployNotice>?
                }, {
                    Log.d(TAG, "" + it)
                })
        )
    }

    fun requestMoreNotice(offset: Int) {
        compositeDisposable.add(
            repository.requestMoreNotice(offset)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { showProgress() }
                .doAfterTerminate { hideProgress() }
                .subscribe({ notices ->
                    val pagingNoticeList = _noticeList.value
                    pagingNoticeList?.addAll(notices)
                    _noticeList.value = pagingNoticeList
                }, {
                    Log.d(TAG, "" + it)
                })
        )
    }

    companion object {
        const val TAG = "EmployNoticeViewModel"
    }
}

 

 


 

 

 

 

[바인딩어댑터]

다른 예제코드와 달리 액티비티에서 Observer 콜백으로 안받고 데이터바인딩 어댑터로 세팅했습니다.

대부분의 예제가 ViewModel LiveData를 액티비티에서 콜백받아 submitList()하는데 저는 다르게 했는데 밑과 같은 이유가 발생하는데 해결은 하긴했지만 아직 정확한 원인을 못찾고있습니다. 

(만약 보시고 원인을 아시면 답변해주시면 감사하겠습니다. ㅠㅠ)

 

밑 데이터바인딩어댑터 적은 주석 이슈는 정확한 원인은 잘 모르지만 

@BindingAdapter("setEmployItems")
fun RecyclerView.setEmployAdapterItems(items: List<EmployNotice>?) {
    items?.let {
        (adapter as EmployAdapter).submitList(it.toMutableList()) 
        //그냥 it으로 넣으면 첫 데이터만 불러와지고 이후 무한스크롤 데이터들이 갱신이 안된다. 
        //페이징 데이터는 여기까지 잘넘어오는데 갱신은 안되는 이유 알아봐야할 것 같음
    }
}


 

밑과 같은 이유같기 때문인거같기도..

 

stackoverflow.com/questions/53407419/pagedlistadapter-submitlist-behaving-weird-when-updating-existing-items

 

PagedListAdapter.submitList() Behaving Weird When Updating Existing Items

Little story of this topic : the app just updating clicked row's values with dialog when confirmed. Uses pagination scenario on room database. When an item added or removed, the latest dataset is

stackoverflow.com

 

 

 

무한스크롤 관련 코드

@BindingAdapter("employEndlessScroll")
fun RecyclerView.setEmployEndlessScroll(
    viewModel: EmployNoticeViewModel
) {
    val scrollListener =
        object : EndlessRecyclerViewScrollListener(layoutManager as LinearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
                viewModel.requestMoreNotice(totalItemsCount + 1)
            }
        }
    this.addOnScrollListener(scrollListener)
}

 

 

 

 

댓글과 공감은 큰 힘이 됩니다. !!

728x90
Comments