Android

[Android] RecyclerView - ViewHolder의 재사용과 상태 관리의 중요성 (선택 이벤트 처리)

도우 2024. 3. 1. 18:32
반응형

 

출처 : https://blog.hexabrain.net/363

 

리스트 항목을 선택하고 스크롤 했더니 선택된 항목이 사라졌어요



문제 분석

RecyclerView는 Android 앱 개발에서 리스트 같은 형태로 아이템을 표시하기 위해 널리 사용되는 컴포넌트이다.

RecyclerView를 사용해서 아이템 선택 시, 색상이 바뀌도록 목록을 구현하고 있었는데 리스트 목록을 아래로 내리니
선택했던 아이템이 사라지는 현상이 발생했다.

즉, 기본적으로 RecyclerView는 화면에 보이지 않는 뷰 홀더를 폐기하고, 다시 뷰에 데이터를 바인딩해서 재사용하기 때문에 기존에 선택했던 아이템이 사라지는 것이었다...!

게다가 선택된 상태로 아래에 다시 등장하는 아이템을 보고, 문제를 해결하기 위해 변경 사항을 추적하기로 했다.

 

    @Override
    public void onBindViewHolder(@NonNull RvAdapter.MyViewHolder holder, int position) {
        boolean isSelected = selectedPosition == position;
        
        holder.itemView.setOnClickListener(v -> {
            int currentPosition = getAdapterPosition();
            if ( selectedPosition == currentPosition ){
                tv.setBackgroundColor(Color.WHITE);
            } else {
                mSelectedItems.put(position, true);
                tv.setBackgroundColor(Color.BLUE);
            }
        });
    }

▲ 현재 코드의 문제점

  1. 변수 재선언 
    position이 onBindViewHolder 메소드의 파라미터로 이미 선언되어 있기 때문에 동일한 이름으로 선언할 수 없다.

  2. 동적인 position
    스크롤이 발생하면 뷰 홀더는 재사용되기 때문에 position이 실제 위치와 일치하지 않을 수 있다.
  3. getAdapterPosition()은 deprecated 되었다.
    getAdapterPosition()은 상대적 위치인지 절대적 위치인지 혼동을 줄 수 있으므로 사용하지 않는 것이 좋다.

 

그렇다면 선택된 position과 현재의 position을 올바르게 저장만 할 수 있다면 문제는 해결된다 !

 

 

문제 해결

private int selectedPosition = RecyclerView.NO_POSITION;

우선 선택된 인덱스를 저장하기 위한 변수를 ViewHolder 외부에 선언한다.

 

   @Override
    public void onBindViewHolder(@NonNull RvAdapter.MyViewHolder holder, int position) {
        boolean isSelected = selectedPosition == position;
        holder.bind(getItem(position), isSelected);

        holder.itemView.setOnClickListener(v -> {
            int currentPosition = holder.getBindingAdapterPosition();
            if (currentPosition != RecyclerView.NO_POSITION){
                if (selectedPosition == currentPosition) {
                    // 현재 선택된 아이템을 다시 클릭했을 경우, 선택 해제 로직
                    notifyItemChanged(selectedPosition); // 이전 상태를 업데이트 (선택 해제를 위해)
                    selectedPosition = RecyclerView.NO_POSITION; // 선택 해제
                    listener.onItemClick(-1); // 선택 해제 상태를 외부로 알림
                } else {
                    // 새로운 아이템을 선택하는 경우
                    int previousSelectedPosition = selectedPosition;
                    selectedPosition = currentPosition;
                    // 이전 선택된 아이템과 새로 선택된 아이템을 업데이트
                    if (previousSelectedPosition >= 0) {
                        notifyItemChanged(previousSelectedPosition); // 이전 선택 해제
                    }
                    notifyItemChanged(selectedPosition); // 새로운 선택 표시
                    listener.onItemClick(currentPosition); // 새로운 선택 상태를 외부로 알림
                }
            }
        });
    }
    
public static class MyViewHolder extends RecyclerView.ViewHolder{
        private RowBinding binding;

        public MyViewHolder(RowBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

        public void bind(Integer number, boolean isSelected){
            binding.numText.setText(String.valueOf(number));
            binding.cardView.setCardBackgroundColor(isSelected ? Color.LTGRAY : Color.WHITE);
        }

    }

 

⇒ 나의 문제 해결 방식

RvAdapter에서 selectedPosition이라는 변수를 사용해서 현재 선택된 아이템의 위치를 추적했다.

아이템 클릭 이벤트가 발생할 때, selectedPosition의 값을 업데이트하고, 변경된 상태를 반영하기위해 notifyItemChanged를 호출하여 해당 아이템 뷰를 재바인딩 했다.

notifyItemChanged() 메소드를 수동으로 호출하는 것은 UI 변경 사항이므로, 우리가 직접 명시적으로 관리하여야 한다.
이때, 불필요한 뷰 업데이트를 피하기 위해 실제로 상태가 변경될 때만 메소드를 호출한다. 

 

정리

  • RecyclerView는 스크롤 성능 최적화를 위해 화면에 보이는 아이템 뷰를 밖에 있는 뷰로 재사용한다.
  • 아이템의 선택 상태와 같은 동적인 상태 정보는 ViewHolder또는 외부 데이터 구조에 저장되어 관리해야 한다.
  • 선택된 아이템의 상태가 변경될 때(선택됨, 선택 안 됨), 이 상태를 반영하기 위해 notifyItemChanged()와 같은 적절한 메소드를 사용하여야 한다.

 

 

 

결과

스크롤을 올려도 선택 상태가 해제되지 않는다.

 

반응형