Android RecyclerView 데이터셋 업데이트 하기

timothy jeong·2021년 12월 8일
0

Android with Kotlin

목록 보기
63/69

리사이클러 뷰에 데이터셋이 추가되어서 notifyDataSetChanged() 를 사용하는 경우가 흔하다. 하지만 리사이클러뷰를 업데이트하는 함수가 이것뿐만 있는게 아니고, 이 함수를 남발할 경우 성능에 악영향을 줄 수 있다.

리사이클러뷰 어댑터에서 제공하는 방법들

전체 갱신

  • notifyDataSetChanged()
    전체 list 를 다시 그린다. 리스트의 크기와 아이템이 둘 다 변경되는 경우에 사용한다.
    일반적인 스크롤의 경우 리스트의 크기만 변경되는데, 매번 이 함수를 호출하는 것은 불필요한 리소르를 사용하는 것이다.

부분 변화 갱신

특정 항목만 갱신(애니메이션, 업데이트)할 때 사용할 수 있다.

  • notifyItemChanged(position: Int)
  • notifyItemChanged(position: Int, payload: Any?)
  • notifyItemRangeChanged(positionStart: Int, itemCount: Int)
  • notifyItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?)

파라미터 payload 는 어댑터의 onBindViewHolder() 가 호출될 때 넘겨받는 객체이다.
payload 를 고려하지 않은 onBindViewHolder 가 아니라,

fun onBindViewHolder(holder: TaskItemViewHolder, position: Int)

이러한 onBindViewHolder 를 따로 정의해 줘야한다.

onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads)

onBindViewHolder() 는 payload 를 받지 않거나, null 을 받는다면 view 를 초기하 해주는 로직이 들어간다. 그 외의 경우는 따로 처리를 해주면 된다. 가령 예를 들면 이런 경우다.

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
    if (payloads.isEmpty()) {
        super.onBindViewHolder(holder, position, payloads);
    }else {
        for (Object payload : payloads) {
            if (payload instanceof String) {
                String type = (String) payload;
                if (TextUtils.equals(type, "click") && holder instanceof TextHolder) {
                    TextHolder textHolder = (TextHolder) holder;
                    textHolder.mFavorite.setVisibility(View.VISIBLE);
                    textHolder.mFavorite.setAlpha(0f);
                    textHolder.mFavorite.setScaleX(0f);
                    textHolder.mFavorite.setScaleY(0f);
                    
                    //animation
                    textHolder.mFavorite.animate()
                            .scaleX(1f)
                            .scaleY(1f)
                            .alpha(1f)
                            .setInterpolator(new OvershootInterpolator())
                            .setDuration(300);

                }
            }
        }
    }
}

payload 파라미터로 String "click" 이 들어온 경우에 대해서 애니메이션을 처리해준다.

부분 삽입 갱신

  • notifyItemInserted(position: Int)
  • notifyItemRangeChanged(positionStart: Int, itemCount: Int)

부분 삭제 갱신

  • notifyItemRemoved(position: Int)
  • notifyItemRangeRemoved(positionStart: Int, itemCount: Int)

부분 이동 갱신

  • notifyItemMoved(fromPosition: Int, toPosition: Int)

DiffUtil 을 이용한 방법

DiffUtil 클래스는 두 list 사이의 차이를 찾아내는데 특화된 클래스이다.
리사이클러 뷰 어댑터 클래스의 상속 구조 자체를 바꿔야하는 방법이다. 그 부분이 다소 낯설 수 있지만, 그점만 감내한다면 별도의 코드 처리 없이 데이터셋 변화에 대해 효과적으로 대응할 수 있다.

적용 방법

우선 DiffUtil.ItemCallback<T> 를 상속하고,
areItemsTheSame(T oldItem, T newItem) 와 areContentsTheSame(T oldItem, T newItem) 를 구현해야한다. 각각은 두 item 들이 동일한 item 을 나타내는지, 두 item 이 동일한 데이터를 갖는지 구분하는 방법을 구현하는 함수이다.

class TaskDiffItemCallback: DiffUtil.ItemCallback<Task>() {

    // 두 변수가 동일한 item 을 참조하고 있는지 판단한다.
    override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean =
        oldItem.taskId == newItem.taskId

    // 두 object 가 동일한 content 를 가졌는지 판단한다.
    // Task 클래스가 data 클래스이므로 이렇게 체크하면 됨.
    override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean =
        oldItem == newItem
}

그리고 리사이클러뷰 어댑터가 ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) 를 상속하도록 바꿔야한다.

  class TaskItemAdapter:
    ListAdapter<Task, TaskItemAdapter.TaskItemViewHolder>(TaskDiffItemCallback()) { ... }

이제는 getItemCount 도 필요없고, 자체적으로 갖는 data 변수도 필요하지 않다.

적용 전 어댑터 코드

class TaskItemAdapter : RecyclerView.Adapter<TaskItemAdapter.TaskItemViewHolder>() {

    var data = listOf<Task>()
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    override fun getItemCount(): Int = data.size

    class TaskItemViewHolder(val rootView: CardView) : RecyclerView.ViewHolder(rootView) {

        val taskName = rootView.findViewById<TextView>(R.id.task_name)
        val taskDone = rootView.findViewById<CheckBox>(R.id.task_done)


        companion object {
            fun inflateFrom(parent: ViewGroup): TaskItemViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val view = layoutInflater.inflate(R.layout.item_task, parent, false) as CardView
                return TaskItemViewHolder(view)
            }
        }

        fun bind(item: Task) {
            taskName.text = item.taskName
            taskDone.isChecked = item.taskDone
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskItemViewHolder =
        TaskItemViewHolder.inflateFrom(parent)


    override fun onBindViewHolder(holder: TaskItemViewHolder, position: Int) {
        val item =  data[position]
        holder.bind(item)
    }
}

적용 후 어댑터 코드

class TaskItemAdapter:
    ListAdapter<Task, TaskItemAdapter.TaskItemViewHolder>(TaskDiffItemCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskItemViewHolder =
        TaskItemViewHolder.inflateFrom(parent)


    override fun onBindViewHolder(holder: TaskItemViewHolder, position: Int) {
        // ListAdapter 에서 제공되는 함수
        val item =  getItem(position)
        holder.bind(item)
    }

    class TaskItemViewHolder(val rootView: CardView) : RecyclerView.ViewHolder(rootView) { ... }
}

프레그먼트 코드 변경

변경 전

        viewModel.tasks.observe(viewLifecycleOwner, Observer {
            it?.let {
                adapter.data = it
            }
        })

변경 후

        viewModel.tasks.observe(viewLifecycleOwner, Observer {
            it?.let {
                adapter.submitList(it)
            }
        })
profile
개발자

0개의 댓글