소개

리스트 모양으로 보여줄 수 있는 위젯으로 리사이클러뷰(RecyclerView)가 있다. 리사이클러뷰는 기본적으로 상하 스크롤이 가능하지만 좌우 스크롤도 만들 수 있다. 왜냐하면 처음 만들어질 때부터 레이아웃을 유연하게 구성할 수 있도록 설계되었기 때문이다. 그리고 이름에서 살펴볼 수 있듯이 뷰 객체를 '재활용(Recycle)'한다는 느낌 그대로 각각의 아이템이 화면에 보일 때 메모리를 효율적으로 사용하도록 캐시(Cache) 메커니즘이 구현되어 있다.

구현

액티비티에 리사이클러뷰 추가

액티비티에 리사이클러뷰를 추가하는 방법은 간단하다. 좌측 상단의 팔레트에서 RecyclerView를 끌어다 화면에 놓으면 된다.

<androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

모델 클래스 정의

public class Person {
    String name;
    String phoneNumber;

    public Person(String name, String phoneNumber) {
        this.name = name;
        this.phoneNumber = phoneNumber;
    }

    public String getName() {
        return name;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setName(String name) {
				this.name = name;
		}

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }
}

아이템 뷰 레이아웃 추가

리사이클러뷰의 아이템 뷰 레이아웃을 생성한다. 여기서는 사용자의 프로필을 나타내는 이미지뷰와 이름, 전화번호를 나타내는 텍스트뷰를 추가했다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="8dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:padding="5dp"
            app:srcCompat="@drawable/ic_person" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tvPersonName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="이름"
                android:textSize="22sp"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/tvPersonPhoneNumber"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="전화번호"
                android:textSize="14sp" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

뷰홀더(ViewHolder)

이름에서 느낌이 오듯 뷰홀더는 간단히 말하면 뷰를 보관하는 객체이다. 아래에서 뷰홀더의 역할을 좀 더 자세히 살펴보자.

리스트뷰와의 차이점

리사이클러뷰의 조상 격인 리스트뷰에서는 스크롤을 하는 동안 상단이나 하단에 표시될 뷰 객체를 새롭게 생성하고 빈번하게 findViewById()를 호출하여 성능 저하가 일어났다.

public class MyAdapter extends BaseAdapter {
    // ...

    // 새로운 아이템을 표시할 때마다 호출된다.
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView == null)
        	convertView = getLayoutInflater().inflate(R.layout.my_list_custom_row, parent, false);
        MyObject myObject = getItem(position);

        // 매번 findViewById()가 호출되어 성능 저하가 일어난다.
        ((TextView) convertView.findViewById(R.id.name).setText(myObject.getName());
        return convertView;
    }
}

안드로이드 팀에서는 이러한 반복 호출을 피하기 위해 뷰홀더 패턴을 사용하길 권장해왔으며 강요는 하지 않았지만, 리사이클러뷰는 뷰홀더 패턴의 사용을 강제하여 성능 저하 문제를 방지하고 있다.

public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.ViewHolder> {
    static class ViewHolder extends RecyclerView.ViewHolder {
        TextView tvPersonName;
        TextView tvPersonPhoneNumber;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);

            tvPersonName = itemView.findViewById(R.id.tvPersonName);
            tvPersonPhoneNumber = itemView.findViewById(R.id.tvPersonPhoneNumber);
        }

        public void setItem(Person item) {
            tvPersonName.setText(item.getName());
            tvPersonPhoneNumber.setText(item.getPhoneNumber());
        }
    }
}

뷰 객체의 재활용

리사이클러뷰에 보이는 여러 개의 아이템은 내부에서 캐시되기 때문에 아이템 개수만큼 객체로 만들어지지는 않는다. 예를 들어, 아이템이 천 개라고 하더라도 이 아이템을 위해 천 개의 뷰 객체가 만들어지지 않는다. 메모리를 효율적으로 사용하려면 뷰홀더에 뷰 객체를 넣어두고 사용자가 스크롤하여 보이지 않게 된 뷰 객체를 새로 보일 쪽에 재사용하는 것이 효율적이기 때문이다. 이 과정에서 뷰홀더가 재사용된다.

위의 그림에서 12번째 아이템을 화면에 표시하기 위해 findViewById() 메서드를 일일이 호출하여 레이아웃에 데이터를 바인딩하지 않고, 기존에 사용했지만 지금은 쓰이지 않는 뷰를 재활용하여 이미 만들어진 것에 데이터를 바인딩한다.

어댑터(Adapter)

어댑터를 구현할 때는 아래와 같은 세 개의 핵심 메서드를 재정의해야 한다.

  • onCreateViewHolder(): 리사이클러뷰는 뷰홀더를 새로 만들어야 할 때마다 이 메서드를 호출한다. 이 메서드는 위에서 정의한 아이템 뷰 레이아웃을 이용해 뷰 객체를 만든다. 그리고 뷰 객체를 새로 만든 뷰홀더 객체에 담아 반환한다.
  • onBindViewHolder(): 리사이클러뷰는 뷰홀더를 데이터와 연결할 때 이 메서드를 호출한다.
  • getItemCount(): 어댑터에서 관리하는 아이템의 개수를 반환한다. 이 메서드는 리싸이클러뷰에서 어댑터가 관리하는 아이템의 개수를 알아야 할 때 사용된다.
public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.ViewHolder> {
    ArrayList<Person> items = new ArrayList<Person>();

    // 뷰홀더가 새로 만들어질 때 호출된다.
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // 파라미터로 전달되는 뷰그룹 객체는 각 아이템을 위한 뷰그룹 객체이므로
        // XML 레이아웃을 인플레이션하여 이 뷰그룹 객체에 전달한다.
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View itemView = inflater.inflate(R.layout.person_item, parent, false);

        return new ViewHolder(itemView);
    }

    // 뷰홀더가 재사용될 때 호출된다. 이 메서드는 재활용할 수 있는 뷰홀더 객체를 파라미터로 전달한다.
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        // 뷰 객체는 기존 것을 그대로 사용하고 데이터만 바꿔준다.
        Person item = items.get(position);
        holder.setItem(item);
    }

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

    public void addItem(Person item) {
        items.add(item);
    }

    public void setItems(ArrayList<Person> items) {
        this.items = items;
    }

    public Person getItem(int position) {
        return items.get(position);
    }

    public void setItem(int position, Person item) {
        items.set(position, item);
    }

    // 위에서 정의한 뷰홀더
    // ...
}

레이아웃 매니저(LayoutManager)

리사이클러뷰는 아래와 같이 일반적으로 쓰이는 3가지의 레이아웃 매니저를 제공한다.

  • LinearLayoutManager: 항목을 1차원 목록으로 정렬한다.
  • GridLayoutManager: 모든 항목을 2차원 그리드로 정렬한다.
  • StaggeredGridLayoutManager: GridLayoutManager와 비슷하지만, 행의 항목이 같은 높이를 가지거나 열의 항목이 같은 너비를 가질 필요가 없다.
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(layoutManager);

리사이클러뷰에 어댑터 연결

public class MainActivity extends AppCompatActivity {
    RecyclerView recyclerView;
    PersonAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        recyclerView = findViewById(R.id.recyclerView);

        LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(layoutManager);
        adapter = new PersonAdapter();

        adapter.addItem(new Person("이름1", "010-1000-1000"));
        adapter.addItem(new Person("이름2", "010-2000-2000"));
        adapter.addItem(new Person("이름3", "010-3000-3000"));
        adapter.addItem(new Person("이름4", "010-4000-4000"));
        adapter.addItem(new Person("이름5", "010-5000-5000"));
        adapter.addItem(new Person("이름6", "010-6000-6000"));
        adapter.addItem(new Person("이름7", "010-7000-7000"));
        adapter.addItem(new Person("이름8", "010-8000-8000"));
        adapter.addItem(new Person("이름9", "010-9000-9000"));
        adapter.addItem(new Person("이름10", "010-1000-1000"));
        adapter.addItem(new Person("이름11", "010-1100-1100"));
        adapter.addItem(new Person("이름12", "010-1200-1200"));
        adapter.addItem(new Person("이름13", "010-1300-1300"));
        adapter.addItem(new Person("이름14", "010-1400-1400"));

        // 리사이클러뷰에 어댑터를 연결한다.
        recyclerView.setAdapter(adapter);
    }
}

'정리 > Android' 카테고리의 다른 글

2021. 10. 19  (0) 2021.10.19
2021. 10. 18  (0) 2021.10.18
메모. ConstraintLayout  (0) 2019.11.01