RecyclerView DiffUtil 사용기
Writer: 박준걸

RecyclerView 란?

Android에서 List 형태로 구성하고 싶을 때 사용하는 View 입니다. ListView 부터 시작해서 오랜 시간 업그레이드 되온 View 입니다. ViewHolder 패턴을 활용해 View를 재활용해서 사용할 수 있어서 메모리 관리에 매우 효율적 입니다.

DiffUtil 이란?

DiffUtil이란 List Item이 변경되었을 때 이전 List Item과 새롭게 변경된 List Item을 비교해서 업데이트 작업을 수행하는 유틸리티 클래스 입니다. 변경된 Item만 업데이트 하는 방식으로 이전에는 List Item이 변경되면 다 다시 그리기 시작했다면, 이제는 변경된 Item만 다시 그리는 방식을 하고 있어서, 효율적으로 퍼포먼스를 낼 수 있습니다.
아래의 내용은 Android Developers에 있는 퍼포먼스 관련 설명 입니다.

사용 방법은?

ListAdaper를 활용하는 방법과 AsyncListDiffer를 활용하는 방법 이렇게 2가지가 있는데요.
ListAdapter로 구현 시 쉽게 구현이 가능하다고하여, 해당 방식으로 구현했습니다.

  1. 기존 코드는 RecyclerView.Adapter로 작성되어 있습니다.
    class AlarmAdapter(val rowClickListener: RowClickListener<AlarmItem>) : RecyclerView.Adapter<AlarmRow>() {
    
     var items: ArrayList<AlarmItem> = ArrayList()
    
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmRow {
         val view = LayoutInflater.from(parent.context).inflate(R.layout.row_user_alarm, parent, false)
         return AlarmRow(view, rowClickListener)
     }
    
     override fun onBindViewHolder(holder: AlarmRow, position: Int) {
         holder.setData(items[position])
     }
    
     override fun getItemId(position: Int): Long {
         return items[position].seq?.toLong() ?: 0
     }
    
     override fun getItemCount(): Int {
         return items.size
     }
    }
    
  2. ListAdaper로 변경할 때 부터는 BaseAdapter를 만들어서 반복적으로 사용되는 기능을 우선 구현하였습니다.
    /**
    * rowClickListener : List Click 이벤트
    * diffCallback : DiffUtil 비교 Callback
    **/
    abstract class BaseAdapter<T>(val rowClickListener: RowClickListener<T>, diffCallback: DiffUtil.ItemCallback<T>) : ListAdapter<T, BaseRow<T>>(diffCallback) {
    
     /**
     * Item을 Binding 하는 함수가 반복적으로 사용되어 Base에 선언
     **/
     override fun onBindViewHolder(holder: BaseRow<T>, position: Int) {
         holder.setData(currentList[position])
     }
      
     /**
     * Item을 갯수를 설정하는 함수가 반복적으로 사용되어 Base에 선언
     **/
     override fun getItemCount(): Int {
         return currentList.size
     }
    
     /**
     * 특정 위치의 Item 삭제
     **/
     fun removeAt(position: Int) {
         val tempList = currentList
    
         if (position < currentList.size) {
             tempList.removeAt(position)
         }
         submitList(tempList)
     }
    
     /**
     * Item을 제일 끝에 추가
     **/
     fun add(item: T) {
         val tempList = currentList
         tempList.add(item)
         submitList(tempList)
         notifyItemInserted(tempList.size - 1)
     }
    
     /**
     * 특정 위치의 Item을 추가
     **/
     fun add(position: Int, item: T) {
         val tempList = currentList
         tempList.add(position, item)
         submitList(tempList)
     }
    
     /**
     * 특정 위치의 Item을 변경
     **/
     fun modify(position: Int, item: T) {
         val tempList = currentList
         tempList.removeAt(position)
         tempList.add(position, item)
         submitList(tempList)
     }
    
     /**
     * Item List를 초기화
     **/
     fun clear() {
         submitList(arrayListOf())
     }
    
     /**
     * Item List를 추가
     **/
     fun addAll(items: ArrayList<T>) {
         val tempList = currentList.toMutableList()
         tempList.addAll(items)
         submitList(tempList)
     }
    
     /**
     * 특정 위치에 Item List를 추가
     **/
     fun addAll(position: Int, items: ArrayList<T>) {
         val tempList = currentList.toMutableList()
         tempList.addAll(position, items)
         submitList(tempList)
     }
    }
    
  3. 실제 BaseAdapter를 적용해봤습니다.
    /**
    * 기존 반복적인 코드는 정리되었으며, ViewHolder의 정의만 하면 작동되는 방식 입니다.
    **/ 
    class AlarmAdapter(rowClickListener: RowClickListener<AlarmDto>) : BasekAdapter<AlarmDto>(rowClickListener, diffCallback) {
    
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmRow {
         val view = LayoutInflater.from(parent.context)
             .inflate(R.layout.row_user_alarm, parent, false)
         return AlarmRow(view, rowClickListener)
     }
    
     companion object {
         /**
         * 클래스가 초기화 시에 DiffUtil을 전달해야 하기 때문에 static으로 선언해서 구현
         **/
         private val diffCallback = object : DiffUtil.ItemCallback<AlarmDto>() {
    
             /**
             * Item 비교
             **/ 
             override fun areItemsTheSame(oldItem: AlarmDto, newItem: AlarmDto): Boolean {
                 return oldItem == newItem
             }
    
             /**
             * Item 안에 값 비교
             **/ 
             override fun areContentsTheSame(oldItem: AlarmDto, newItem: AlarmDto): Boolean {
                 return oldItem.seq == newItem.seq
             }
         }
     }
    }
    

    areItemsTheSame가 true가 되면 areContentsTheSame를 호출해서 다시 한번 더 확인을 합니다.
    매우 중요한 포인트 입니다. 참고로 저는 개발하면서 이부분을 놓치는 바람에, 롤백하였습니다.

이슈!

실제로 적용을 했지만, 이전에서는 없었던 깜박임 현상이 발생하였습니다.
일부 데이터가 변경되거나, 추가 데이터가 생길 때마다 깜박이는 현상으로 인해 사용이 불가능하다고 판단 다시 코드 롤백을 진행하였습니다.

아쉬움…

이번에 블로그를 작성하면서 다시 확인해보니 동일하게 이런 현상이 있던 사람들이 있었고, 확인해보니 areItemsTheSame 부분을 단순히 == 가 아닌 데이터 값으로 비교해서 작동 시 깜박임 없이도 잘 작동 되는 것을 확인하였습니다.
이 내용을 개발할 당시에 알았다면, 적용 완료하였겠지만… 지금은 늦어버린 시점이 되어 버려서 나중에 다시 DiffUtil에 도전에서 성공한 케이스로 다시 작성할 수 있도록 하겠습니다.