Android 유연성 있는 ViewHolder Pattern

얼마전 ListView 포퍼먼스 팁에 관한 블로그 포스팅을 한적이 있다.  Adapter에서 View의 재활용과 함께 ViewHolder Pattern으로 findViewById를 View생성 시점에 setTag()를 하여 재활용에 대해 언급 했다. 

이 방법은 각 ListView의 ViewItem별로 각각의 ViewHolder를 가지고 있어야 한다.  

ListView의 아이템별로 서로 다른 디자인이 필요하기에 View의 종류가 달라 질 수 밖에 없기때문에 ViewHolder도 각각 존재 할 수 밖에 없다. 이렇게 static하게 ViewHolder을 가지고 있는것 보다 유연하게 ViewHolder를 생성 할 수 있는 코드를 생성하는 방법에 대해서 알아보자.

아래처럼 Adapter에 static class로 만들어 ViewHolder를 사용하는것이 일반적이다.  ViewHolder는 같은 기능을 하는데 단순히 View의 종류가 달라져 각각 생성 할수 밖에 없는 상황이다. 

private static class ViewHolder {

    public final ImageView bananaView;
    public final TextView phoneView;

    public ViewHolder(ImageView bananaView, TextView phoneView) {
        this.bananaView = bananaView;
        this.phoneView = phoneView;
    }
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {

    ImageView bananaView;
    TextView phoneView;

    if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.banana_phone, parent, false);
        bananaView = (ImageView) convertView.findViewById(R.id.banana);
        phoneView = (TextView) convertView.findViewById(R.id.phone);
        convertView.setTag(new ViewHolder(bananaView, phoneView));
    } else {
        ViewHolder viewHolder = (ViewHolder) convertView.getTag();
        bananaView = viewHolder.bananaView;
        phoneView = viewHolder.phoneView;
    }

    BananaPhone bananaPhone = getItem(position);
    phoneView.setText(bananaPhone.getPhone());
    bananaView.setImageResource(bananaPhone.getBanana());

    return convertView;
}

해결책으로 아래와 같이 ViewGroup의 View들에 대해 동적으로 생성하는 코드를 만들 수 있다.

public class ViewHolder {
    @SuppressWarnings("unchecked")
    public static <T extends View> T get(View view, int id) {
        SparseArray<View> viewHolder = (SparseArray<View>) view.getTag();
        if (viewHolder == null) {
            viewHolder = new SparseArray<View>();
            view.setTag(viewHolder);
        }
        View childView = viewHolder.get(id);
        if (childView == null) {
            childView = view.findViewById(id);
            viewHolder.put(id, childView);
        }
        return (T) childView;
    }
}

모든 View들은 View를 상속 받고 있기 때문에  SparseArray를 통해서  ChildView의 Id가 없으면 findViewById()를 통해 put해주고 있으면 get으로 가져오는 방식이다. 

사용하는 간단한 예를 알아보면:

@Override
public View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.banana_phone, parent, false);
    }

    ImageView bananaView = ViewHolder.get(convertView, R.id.banana);
    TextView phoneView = ViewHolder.get(convertView, R.id.phone);

    BananaPhone bananaPhone = getItem(position);
    phoneView.setText(bananaPhone.getPhone());
    bananaView.setImageResource(bananaPhone.getBanana());

    return convertView;
}

기존의 방법보다 더 간결화되고 심플해진 것을 볼 수 있다.

“Android 유연성 있는 ViewHolder Pattern”에 대한 9개의 생각

  1. 코드가 간결해지고 짱 좋네요.

    음.. 그런데 살펴보니 Null Pointer Error가 날 이유가 없어보이는데
    가끔 나더군요… 원인이 뭘까요??

  2. 혹시 재활용 View가 동일하지 않고, 포지션이나 상황에 따라 다른 View를 생성하신다면 null이 떨어 질 가능성이 있을것 같네요..

  3. 좋은 내용입니다만, 아쉽게도 원본 출처나 동의 없이 게시된 글 같아보이는데 적절한 설명이 필요할 것 같습니다. 대부분 내용들이 그러하네요. 아쉽습니다.

  4. 보통 블로그에 직접 글을 작성하며, 참고한 내용은 대부분 하단 또는 본문중에 출처를 남기고 있습니다.

  5. 좋은글 감사합니다^^

    그런데 이해가 잘 안가는 부분이 있습니다.

    질문1.
    ViewHolder의 get() 메서드를 static으로 선언하셨는데 ViewHolder가 static 클래스가 아니라서 클래스안에 static 메서드를 만들수없다고 에러가 뜨더라구요..
    (The method get cannot be declared static; static methods can only be declared in a static or top level type)
    위 코드와는 다르게 ViewHolder 클래스를 static으로 선언해야 하는건가요?

    질문2.
    SparseArray로 만들어진 ViewHolder도 convertView.getTag()로 불러와서 각각의 converView에 하나씩, 여러개의 ViewHolder를 생성하는데 무엇이 좋아진건지 잘 이해가 안갑니다 ㅠㅠ

  6. 안녕하세요^^
    1. 위 코드는 예시일 뿐이지 직접 사용하실려면 static class로 만드시면 됩니다.
    2. 기존 ViewHolder 패턴은 Object로 만들어서 convertView에 Tag로 저장하는 방법인데, 이 ViewHolder패턴은 각각의 Adapter에 따라 달라지게 됩니다. 그런데 SparseArray를 이용해서 동적으로 필요 할때 마다 생성하는 장점이 있습니다. 물론 Adapter에따라 ViewHolder Object를 가지고 있지 않아도 되구요.

    감사합니다.

  7. 좋은 글 감사합니다!
    괜찮으시다면 출처 남기고 코드만 인용해도 될까요?

댓글 남기기