땀이 삐질삐질 나는 개발 일기

Recyclerview와 ViewHolder의 개념 본문

개발 Tip

Recyclerview와 ViewHolder의 개념

삐질 2019. 7. 15. 20:27
안녕하세요. 삐질삐질 개발하는 개발자 삐질입니다.


이번 글에서는 리사이클러뷰와 뷰홀더에 대해 알아보도록 하겠습니다.
이름에서 알수 있듯이 리사이클러뷰= 재활용 , 뷰홀더 = 그릇 이라는 의미를 유추할 수 있죠?

맞습니다. 리사이클러뷰는 뷰홀더를 재사용하는 View입니다.

무슨 뜻인지 좀더 자세하게 알아보도록 할게요.
우리는 주로 아래와 같은 리스트를 표현하는데 있어서 리사이클러뷰를 사용합니다. 

(*주의* 리사이클러뷰를 한번도 써보지 않은 분은 반드시 사전에 따로 리사이클러뷰의 사용법을 찾아보세요)




리사이클러뷰의 어댑터 코드는 아래와 같습니다. (예시, 맨 아래의 RecyclerAdapter와 관련 x)
그렇구나 ㅡㅡ;; 하고 넘기시면 되는 코드에요!

RecyAdapter.Java
public class RecyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private List<String> items;
    private ClickCallbackListener callbackListener;

    public RecyAdapter() {
        items = new ArrayList<>();
    }

    public void setItems(List<String> items) {
        this.items.addAll(items);
        notifyDataSetChanged();
    }

    //메인액티비티에서 전달 받은 콜백메서드를 set 하는 메서드
    public void setCallbackListener(ClickCallbackListener callbackListener) {
        this.callbackListener = callbackListener;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ItemTextBinding binding = ItemTextBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
        return new ItemViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof ItemViewHolder) {
            String item = items.get(position);
            /*해당 아이템과 callbackListener를 바인드 된 뷰홀더에 전달한다.*/
            ((ItemViewHolder) holder).bind(item, callbackListener);
        }
    }

    @Override
    public int getItemCount() {
        return items.size();
    }


    public static class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        private ItemTextBinding binding;
        private ClickCallbackListener callbackListener;

        public ItemViewHolder(ItemTextBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

        public void setListener() {
            binding.rootView.setOnClickListener(this);
        }

        public void bind(String item, ClickCallbackListener callbackListener) {
            binding.textTitle.setText(item);// 전달받은 String setText
            this.callbackListener = callbackListener; //전달받은 콜백을 해당 뷰홀더의 멤버로 가지고있음.

        }

        @Override
        public void onClick(View v) {
            int id = v.getId();
            /*클릭 후 메인액티비티에서 구현되어 전달 된 콜백 메서드를 호출하여 인자로 클릭 포지션을 담았다.*/
            callbackListener.callBack(getAdapterPosition());
        }
    }
}

- item.xml View 코드 입니다.
<?xml version="1.0" encoding="utf-8"?>
<layout>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rootView"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#E76363"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textTitle"
            android:text="테스트 영역입니다."
            android:textSize="28sp"
            android:gravity="center"
            android:textColor="#ffffff"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>
</layout>

이때 

1번째 Row  

2번째 Row
.
.
.

각각의  칸(Row)은 제가 지정한 xml의  View 형식과, 제가 해당 Row Position과 ArrayList의 인덱스와 일치하는 값으로 표현해줍니다.

여기까지만 설명하면, 리스트뷰지 이게 왜 리사이클러뷰야?? 하시겠죠??

리스트뷰와 리사이클러뷰의 기본적인 동작 구조는 , 내 아이템 갯수만큼  1칸 1칸 지정한 형식의 xml View를 메모리에 인플레이트 시킵니다. 다만 이때 화면에 보이게 되는 아이템만을 인플레이트 시킵니다. 무슨 뜻이냐면, 실제로 내가 가진 ArrayList의 사이즈는 10이지만, 한 화면에 5개까지 밖에 표현할 수 없고, 나머지 5개는 다음 스크롤로 표현해야 한다면,  실제로 만들어지는 칸은 10개가 아닌, 5개만 만들어지게 됩니다. 

그후, 스크롤이 되어 5번째 아이템과 6번째 아이템의 경계면이 화면에 보이는 순간! 6번째 아이템이 
메모리에 인플레이트 되게 됩니다. 마찬가지로 스크롤 해서 6번째 아이템과 7번째 아이템의 경계면이 보이는 순간 7번째 아이템을 인플레이트 하게 됩니다. 

이렇듯 기본적으로 “화면에 보이는 것만 로드하고 보이지 않는 것을 처음부터 모두 만들어두지는 않는다.” 라는 
동작방식을 가지고 있습니다.

하지만 아이템이 100로 늘어났을땐 미리 만들지 않고, 보이는 순간에 만든다고 해도 100개의 칸을 모두 만들어야 하겠죠??(해당 각 칸의 TextView ,ImageView 등등 )  여기서 바로 문제점이 발생합니다.

우리의 스마트폰은 한정된 메모리를 사용하고 있기 때문에 무한정 메모리에 뷰를 인플레이트 시킬 수 없습니다. 계속해서 뷰를 인플레이트 시키다 보면, 결국 Out of Memory를 만날 수 밖에 없는 것이죠.

그렇다 보니 이런 문제점을 해결하고자 , “(Recycle)재사용”이라는 단어가 등장하게 됩니다. 어짜피 똑같은 형식(xml View)으로 , 값만 바꿔서 쓸건데 뭐하러 100칸을 다 만들지?? 그냥 화면에 5칸만 보인다면 한 한 6개쯤 만들어놓고 , 6번째칸이 보이면 1번째 칸이 안 보일테니까 새로 만드는 게 아니라 이미 만들어져있는 1번째 칸을 밑에다 붙여서 값만 바꿔주면 되는거 아냐?? 

하는 답을 얻게됩니다.  그렇기 위해 , xml View와 Data를 담아줄 수있는 Holder의 개념이 등장하게 됩니다. 이로써 ViewHolder를 재활용하는 개념이 적립이 되었고 

ListView에서는 아래와 같이 뷰홀더를 이용합니다.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder viewHolder;

    if (convertView == null) {

        convertView = layoutInflater.inflate(R.layout.item, null);

        viewHolder = new ViewHolder();
        viewHolder.textView1 = (TextView) convertView.findViewById(R.id.textview1);
        viewHolder.textView2 = (TextView) convertView.findViewById(R.id.textview2);
        viewHolder.textView3 = (TextView) convertView.findViewById(R.id.textview3);
        viewHolder.textView4= (TextView) convertView.findViewById(R.id.textview4);
        viewHolder.textView5 = (TextView) convertView.findViewById(R.id.textview5);
        convertView.setTag(viewHolder);

    } else {
        viewHolder = (ViewHolder) convertView.getTag();
    }

    viewHolder.textView1.setText("1번째 텍스트뷰");
    viewHolder.textView2.setText("2번째 텍스트뷰");
    viewHolder.textView3.setText("3번째 텍스트뷰");
    viewHolder.textView4.setText("4번째 텍스트뷰");
    viewHolder.textView5.setText("5번째 텍스트뷰");

    return null;
}
public static class ViewHolder{
    private TextView textView1,textView2,textView3,textView4,textView5;

}
이렇게 한번도 뷰가 인플레이트 되지 않았던 뷰라면, 새로운 뷰홀더를 만들고 그 뷰홀더가 가진 뷰인스턴스에  인플레이트 될 뷰를 매핑해 줍니다.  만약 이미 인플레이트 되었던 뷰라면 태그로 지정해 두었던 뷰홀더를 가지고 와 재사용 하게 됩니다.

이렇듯 수동적으로 사용자가 코드를 작성하다 보니 직관적이지 않고 편의성이 매우 떨어지게 되며, 작성해야 하는 코드는 늘어났습니다. 구글은 “그래! 어짜피 메모리 활용면에서 뷰홀더 패턴을 항상 코드 작성을 하는 게 좋다면 강제할 수 있는 라이브러리를 우리가 만들어줄게!!” 하고 탄생하 것이 리사이클러 뷰 입니다.

리사이클러뷰의 코드는 아래와 같습니다.


@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    if(holder instanceof ItemViewHolder){
        ((ItemViewHolder) holder).textView1.setText("1번째 텍스트뷰");
        ((ItemViewHolder) holder).textView2.setText("2번째 텍스트뷰");
        ((ItemViewHolder) holder).textView3.setText("3번째 텍스트뷰");
        ((ItemViewHolder) holder).textView4.setText("4번째 텍스트뷰");
        ((ItemViewHolder) holder).textView5.setText("5번째 텍스트뷰");
    }
}

@Override
public int getItemCount() {
    return 0;
}

public static class ItemViewHolder extends RecyclerView.ViewHolder{
    private TextView textView1,textView2,textView3,textView4,textView5;
    public ItemViewHolder(@NonNull View itemView) {
        super(itemView);
    }
}

어떤가요? 얼핏 보기엔 몇줄 줄어들진 않았지만, 굳이 제가 뷰와 뷰홀더를 일일이 맵핑해줘야 하는 일이 없어졌습니다.
(개발자는 코드 한줄 한줄이 생산성…!!)

이렇게 우리는 매번 칸이 만들어질 때 마다 뷰를 인플레이트 하는 것이 아닌, 이미 만들어졌지만, 화면에서 사라진 뷰를 재활용 할수있습니다.

간단하게 그림으로 살펴보자면 아래와 같습니다.


위에서 생성했던 뷰홀더를 아래에 붙여서 다시 쓰는거죠. 

다만!! 이렇게 재사용을 하다보면  간혹 예상치 못한 동작이 생길 때가 있습니다.
스크롤을 하다보면 데이터가 뒤죽박죽으로 설정되는 경우인데요.  그 경우가 바로 뷰홀더 재사용을 제대로 이해하지 못한경우 입니다. 일괄적으로 모든 경우에 텍스트뷰 5개의 값을 설정해주는 경우엔 상관없는 이야기지만 ,

조건문을 대칭(if/else)로 쓰지 않고, if만 쓰는 경우 if를 타지 않았을 때 설정하지 않은 View(Textview 나 ImageView 등)재사용하기 위해 이전에 만들어 놓았던 다른 포지션의 뷰홀더의 값이 대입되며 내가 원하는 방향과는 다른 값이 도출되게 됩니다.
ex) 한 마디로 남이 쓰던 헌 옷을 내가 리폼하고 싶은데  왼쪽 팔 부분만 리폼을 했다면 오른쪽은 당연히 헌 그대로일 것이고 그 전사람의 흔적이 남아있겠죠? 
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    if(holder instanceof ItemViewHolder){
       String msg = null;
        if(position==0){
          msg ="삐질 존잘남";
         }    
         else{
             msg="이히히히힣"+position;
          }
       if(msg.equas("삐질 존잘남")){
          ((ItemViewHolder)holder).textview1.setTextColor(파랑색)
       }
    }
}

@Override
public int getItemCount() {
    return 50;
}

public static class ItemViewHolder extends RecyclerView.ViewHolder{
    private TextView textView1,textView2,textView3,textView4,textView5;
    public ItemViewHolder(@NonNull View itemView) {
        super(itemView);
    }
}
이 경우  스크롤을 하다보면, 포지션이 0일때는 삐질존잘남이 일치해서 처음 리사이클러뷰 아이템을 그리게 되면, 정상이었다가, 스크롤을 해서  계속 해서 화면에서 사라진 ViewHolder가  위로붙었다 아래로 붙었다 하다보면  텍스트 뷰 색상이 파란색이 나오면 안될 곳에 파란색이 된다거나 그런 예상하지 않은 현상이 발생하게 됩니다.


이를 꼭 유의하며 개발하시기 바랍니다.


안드로이드 초보 개발자를 위해 아래와 같은 카카오 오픈톡을 운영 중입니다






Comments