Tech & Programming/모바일(Android, Flutter)

ViewHolder 가 무엇인가요?

소스코드 요리사 2019. 4. 30. 18:46

  최근에 제가 안드로이드 면접을 봤었습니다.
RecyclerView 이야기가 나와서 설명하다가 ViewHolder에 대해서 설명해 보라는 면접 질문을 받았었는데, 막상 아는 내용이지만 설명하려고 하니 어렵더군요. 그래서 ViewHolder와 이를 사용하는 ListView와 RecyclerView에 대해서 간략하게 정리를 해 보았습니다.

1. ViewHolder란?

한마디로 설명하면 각 뷰를 보관하는 Holder 객체로 이야기 할 수 있습니다.
  "ListView / RecyclerView 는 inflate를 최소화 하기 위해서 뷰를 재활용 하는데, 이 때 각 뷰의 내용을 업데이트 하기 위해 findViewById 를 매번 호출 해야합니다. 이로 인해 성능저하가 일어남에 따라 ItemView의 각 요소를 바로 엑세스 할 수 있도록 저장해두고 사용하기 위한 객체입니다." 정도로 설명할 수 있을 것 같습니다.

 

※ inflate? : xml 로 쓰여있는 View의 정의를 실제 VIew 객체로 만드는 것을 말함.

 

2. List View 에서 ViewHolder

※ 전체 소스 코드 : https://github.com/JunpilPark/SampleCode-Android/tree/base_listView

 

  getView() 에서 넘어오는 파라메터인 convertView 사용하지 않고 매번 새로운 뷰를 inflate하면 메모리 낭비를 하게 됩니다. 그래서 보통 convertView 가 null 일 때만 inflate 되도록 해 ListView를 위한 Adapter의 getView는 아래와 같이 구현하게 됩니다. 

이때 항상 아이템의 값을 setText하기 위해서 findViewById 를 통해 XML 리소스에 접근하게 되고 이는 성능저하를 나타내는 원인이 된다.

 

UserAdapter.java 파일의 일부

@Override 
    public View getView(int position, View convertView, ViewGroup parent) { 
        if(convertView == null) { 
            convertView = layoutInflater.inflate(R.layout.user_list_item, parent, false); 
        } 
        TextView txtName = convertView.findViewById(R.id.txt_value_name); 
        TextView txtMail = convertView.findViewById(R.id.txt_value_mail); 
        TextView txtMobile = convertView.findViewById(R.id.txt_value_mobile); 

        txtName.setText(userList.get(position).getName()); 
        txtMobile.setText(userList.get(position).getMobile()); 
        txtMail.setText(userList.get(position).getEmail()); 

        return convertView; 
    }

 

  ViewHolder 패턴을 사용하여 아래와 같이 수정이 가능합니다.
이렇게 되면 convertView가 null 일 때 inflate 하고, ViewHolder를 생성해 각요소를 findViewById 를 통해 저장합니다.

그리고, 다음부터는 ViewHolder를 getTag로 불러와서 재사용하게 됩니다. 이렇게하면 xml 리소스에 findeViewById로 직접 접근하지 않아도 됩니다.

  그리고, 참고사항으로 여기서 ViewHolder클래스를 내부 클래스로 만드는 이유는 UserAdapter만 관련이 있고, 소스의 가독성을 높이기 위함입니다. static을 붙히는 이유는 상위클래스의 멤버변수나 객체를 사용하지 않겠라는 것을 명시적으로 표시하기 위함입니다.

 

UserAdapter.java 파일의 일부

public static class UserViewHolder {
    TextView txtName;
    TextView txtMail;
    TextView txtMobile;
    
    public void bind(String name, String mail, String mobile) {
        txtName.setText(name);
        txtMail.setText(mail);
        txtMobile.setText(mobile);
    }
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    UserViewHolder userViewHolder;
    if(convertView == null) {
        LayoutInflater layoutInflater = LayoutInflater.from(context);
        convertView = layoutInflater.inflate(R.layout.user_list_item, parent, false);
        userViewHolder = new UserViewHolder();
        userViewHolder.txtMail = convertView.findViewById(R.id.txt_value_mail);
        userViewHolder.txtName = convertView.findViewById(R.id.txt_value_name);
        userViewHolder.txtMobile = convertView.findViewById(R.id.txt_value_mobile);
        convertView.setTag(userViewHolder);
    }
    else {
        userViewHolder = (UserViewHolder) convertView.getTag();
    }

    userViewHolder.bind(userList.get(position).getName(),
            userList.get(position).getEmail(),
            userList.get(position).getMobile());

    return convertView;
}

 

3. ListView 의 장점, 단점

  이러한 리스트 뷰는 ArrayAdapter 등을 제공하여 간단하고 빠르게 리스트를 만드는 장점이 있습니다.
하지만, 다양한 View Item이 필요하면 복잡한 커스텀작업이 필요하고, 애니메이션 사용이 어렵습니다.
그리고, 위에서 설명한 ViewHolder도 강제하지 않아 매번 inflate와 findViewById 가 호출되어 리소스 비용이 올라 갈 수 있습니다.

4. RecyclerView 의 ViewHolder

  기본적인 RecyclerView의 사용방법은 공식문서(https://developer.android.com/guide/topics/ui/layout/recyclerview)에 잘 나와 있으니 링크를 참조해주시면 될 것 같습니다. 여기서는 ViewHolder 중심으로 쓰도록 하겠습니다.

RecyclerView의 경우는 Recycler.Adapter를 상속 받을 때 ViewHolder Type을 지정해야 합니다.
ListView에서는 ViewHolder 를 구현하지 않아도 되지만, RecyclerView는 구현을 강제하고 있습니다.
Recycler.Adapter를 상속하면, 몇 개의 메소드를 오버라이딩 해야합니다. 그 중  ViewHolder와 관련성이 높은 메소드는 onCreateViewHolder(), onBindViewHolder() 입니다.

 

ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 호출 되면서 새로운 View 를 생성할 때 실행되어 ViewHolder를 리턴합니다. 그리고, void onBindViewHolder(ViewHolder holder, int position) 에서 ViewHolder의 내용을 변경합니다.

아래는 제 github에 있는 샘플 소스코드를 발췌했습니다.

public class UserRecyclerAdapter extends RecyclerView.Adapter<UserRecyclerAdapter.UserViewHolder>{

	public static class UserViewHolder extends RecyclerView.ViewHolder {

		TextView txtName;
		TextView txtMail;
		TextView txtMobile;

		public UserViewHolder(@NonNull View itemView) {
			super(itemView);
			txtName = itemView.findViewById(R.id.txt_value_name);
			txtMail = itemView.findViewById(R.id.txt_value_mail);
			txtMobile = itemView.findViewById(R.id.txt_value_mobile);
		}

		public void bind(String name, String mail, String mobile) {
			txtName.setText(name);
			txtMail.setText(mail);
			txtMobile.setText(mobile);
		}
	}
	
	@NonNull
    @Override
    public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.user_list_item,  parent, false);
        UserViewHolder userViewHolder = new UserViewHolder(view);
        return userViewHolder;
    }

	@Override
    public void onBindViewHolder(@NonNull UserViewHolder userViewHolder, int position) {
        userViewHolder.bind(userList.get(position).getName(),
                            userList.get(position).getEmail(),
                            userList.get(position).getMobile());
    }
}