티스토리 뷰

강의 초반에 여러 아이템 중에 하나를 선택하기 위한 선택 위젯의 개념이 나왔었다. 선택 위젯의 종류로 ListView, GridView, Spinner 등이 있고, 그 중에 '[부스트코스 PJ2 정리노트] 이벤트와 리스트 뷰'에서 ListView의 사용법에 대해 정리한 적 있다. 하지만 RecyclerView의 많은 장점 때문에 ListView보다는 RecyclerView를 많이 사용한다.

 

 

RecyclerView의 장점은 다음과 같다.

  1. ListView는 상하 스크롤만 지원하지만 RecyclerView는 상하 스크롤과 좌우 스크롤을 모두 지원한다. 
    - 이는 처음 만들어질 때부터 레이아웃을 유연하게 구성할 때 있도록 설계되었기 때문이다.

  2. 각각의 아이템이 화면에 보이는 과정에서 뷰홀더(ViewHolder)를 이용해 메모리를 덜 사용하도록 캐시 메커니즘이 구현되어 있다.

RecyclerView는 롤리팝(5.0) 버전부터 안드로이드 SDK에 포함되었기 때문에 지금은 상당히 많은 앱들에서 RecyclerView가 사용되고 있다.

 

 

RecyclerView 구조

RecyclerView 구조

RecyclerView는 큰 구조로 보면 이와 같다.

xml 파일에 <RecyclerView>를 추가하면 껍데기가 생기고, 가로/세로 방향을 설정하는 LayoutManager와 아이템을 관리하는 Adapter를 만들어서 <RecyclerView>에 설정해줘야 한다.

그러면 Adapter에서 Item List를 관리하고, 내부적으로 ViewHolder가 하나의 item 데이터와 하나의 아이템에 대한 item_layout.xml을 결합(Binding)하는 역할을 한다.

이렇게 하면 최종적으로 <RecyclerView>의 내부가 채워져서 앱에 보여지는 것이다.

 

 

RecyclerView 사용 방법

RecyclerView를 사용하기 위해서는 xml 파일에 RecyclerView를 추가해야 하는데, 외부 라이브러리이기 때문에 따로 import를 해야 한다. 직접 라이브러리에서 추가하는 방법과 팔레트에서 RecyclerView를 끌어와서 임포트를 하는 방법이 있다. RecyclerView는 ListView와 같은 선택 위젯이기 때문에 어댑터가 데이터 관리와 뷰 객체 관리를 담당한다. 즉, xml에 추가한 RecyclerView는 껍데기 역할을 하는 것이다.

 

또한 RecyclerView는 방향 설정을 할 수 있는데, 이 때 LayoutManager를 사용한다. LayoutManager를 이용해 RecyclerView가 보일 기본적인 형태를 설정하는데 일반적으로 세로 방향, 가로 방향, 격자 모양을 많이 사용한다. 설정하는 방법은 아래 코드와 같다.

recyclerView = findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(layoutManager);

 

RecyclerView는 각각의 아이템이 뷰로 만들어지며 각각의 아이템을 위한 뷰를 뷰홀더에 담아 두게 된다. 그렇기 때문에 리스트뷰와는 다르게 View 객체를 만들 필요가 없다. (리스트뷰에서는 ItemView.java와 같은 뷰 객체를 만들어 하나의 아이템에 대한 레이아웃을 인플레이션하는 작업을 해야 했다.) 이 뷰홀더 역할을 하는 클래스를 어댑터 클래스 안에 넣어 두면 된다. 

 

그렇기 때문에 하나의 아이템의 xml 레이아웃과 그 안에 들어갈 데이터를 가지고 있는 java 파일을 먼저 만든다.

아래의 xml 파일을 item.xml 파일이라고 하자.

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

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

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

 

이제 하나의 아이템에 대한 Item 클래스를 Item.java로 만들어준다.

class Item {
    private String name;
    private String mobile;

    Item(String name, String mobile) {
        this.name = name;
        this.mobile = mobile;
    }
    
    String getName() { return name; }
    String getMobile() { return mobile; }
}

 

이렇게 아이템에 대한 xml과 java 파일이 준비가 되었으면 이제 실제 데이터를 가지고 RecyclerView에 내용을 보여줄 어댑터를 만들어야 한다. 

먼저, Adapter를 만들고 inner class로 ViewHolder를 static으로 정의한다. 그렇게 하면 리스트 형태로 보일 때 각각의 아이템은 뷰로 만들어지며 각각의 아이템을 위한 뷰는 뷰홀더에 담아 두게 되는데, 이 뷰홀더 역할을 하는 클래스를 Adapter 클래스 안에 넣어둔다고 생각하면 된다. RecyclerView.ViewHolder 클래스를 상속하여 정의된 ViewHolder 클래스의 생성자에는 뷰 객체가 전달된다. 

public class Adapter {

    static class ViewHolder extends RecyclerView.ViewHolder{
        TextView textView;
        TextView textView2;

        // 각각의 item에 대한 뷰가 뷰홀더의 파라미터로 전달 됨.
        ViewHolder(View itemView) {
            super(itemView);

            textView = itemView.findViewById(R.id.textView);
            textView2 = itemView.findViewById(R.id.textView2);
        }

        void setItem(Item item) {
            textView.setText(item.getName());
            textView2.setText(item.getMobile());
        }
    }
    
}

 

이제 이 Adapter 클래스가 RecyclerView.Adapter<Adapter.ViewHolder> 클래스를 상속하도록 수정하고, 필요한 메서드를 Implement 해야 한다. 필요한 메서드와 각각의 메서드가 하는 일은 다음과 같다.

  • getItemCount() - 어댑터에서 관리하는 아이템 개수 반환.
  • onCreateViewHolder() - 뷰홀더 객체가 만들어질 때 호출되는 함수.
  • onBindViewHolder() - 뷰홀더 객체가 재사용될 때 호출되는 함수.

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

 

이제 코드를 작성해 보자.

public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
    private ArrayList<Item> items = new ArrayList<>();

    @Override   // 아이템의 개수 리턴
    public int getItemCount() { return items.size(); }

    @NonNull
    @Override   // 뷰홀더가 만들어지는 시점에 호출되는 메서드 (뷰홀더가 재사용된다면 이 메서드는 호출되지 않는다.)
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View itemView = inflater.inflate(R.layout.item, parent, false);

        return new ViewHolder(itemView);    // itemView를 가지고 있는 뷰 홀더를 만들어서 반환
    }

    @Override   // 데이터와 뷰가 결합되는 시점에 호출되는 메서드
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.setItem(items.get(position));
        // 이렇게 하면 items에 있는 데이터를 알 수 있게 되어 뷰홀더 안에 있는 뷰에 데이터를 설정해줄 수 있다.
    }

    static class ViewHolder extends RecyclerView.ViewHolder{
        ...
    }
}

 

onCreateViewHolder() 메서드에서는 아까 만들었던 item.xml  레이아웃을 인플레이션 해서 뷰 객체로 만들어 준 뒤 그 뷰 객체를 새로 만든 뷰홀더 객체에 담아 반환하는 역할을 한다. 이 때 LayoutInflater.from() 메서드로 인플레이션을 진행하기 위해서는 Context 객체가 필요하기 때문에 파라미터로 전달되는 뷰그룹 객체의 getContext() 메서드를 이용해 Context 객체를 참조할 수 있다. 또한 파라미터로 전달되는 뷰그룹 객체는 각 아이템을 위한 뷰그룹 객체이므로 xml 레이아웃을 인플레이션하여 이 뷰그룹 객체에 설정한다.

 

onBindViewHolder() 메서드는 뷰 객체는 그대로 두고 그 안에 데이터만 바꿔주는 역할을 한다. 데이터는 아까 만든 Item 클래스의 객체로 만드는데 여러 아이템을 어댑터에서 관리해야 하기 때문에 클래스 안에 ArrayList 자료형으로 된 items 라는 변수를 만들어서 관리한다. 그러면 onBindViewHolder() 메서드의 파라미터로 전달된 position을 이용해 ArrayList에서 특정 position에 있는 Item 객체를 꺼내서 설정할 수 있다.

 

어댑터가 ArrayList 안에 들어 있는 전체 아이템의 개수를 알아야 하기 때문에 getItemCount() 메서드에서는 items.size()를 리턴한다.

 

※ 참고로 onCreateViewHolder() 메서드에서 파라미터로 정수 값인 viewType이 전달되는데, 이것은 각 아이템을 위한 뷰를 여러 가지로 나누어 보여주고 싶을 때 사용하는 것이다. 예를 들어, 어떤 때는 이미지를 보여 주고 어떤 때는 이미지와 텍스트를 같이 보여주고 싶다면 뷰 타입을 정하고 각각의 뷰 타입에 따라 다른 xml 레이아웃을 인플레이션 해서 보여주게 하면 된다.

 

이제 어댑터 안의 item을 추가하기 위해 Adapter 클래스 안에 addItem() 메서드를 추가한다.

public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder>{
    private ArrayList<Item> items = new ArrayList<>();

    void addItem(Item item) { items.add(item); }
    
    ...
}

 

마지막으로 MainActivity에서 어댑터 객체를 만들고 아이템을 추가한 뒤 어댑터 객체를 처음에 만든 RecyclerView에 등록해 주면 된다. 여기서 notifyDataSetChanged() 메서드를 호출해야 어댑터가 내부 데이터가 변경되었음을 인지하고 변경된 데이터를 반영한다.

Adapter adapter = new Adapter();

adapter.addItem(new Item("뇨뇨", "010-0000-0000"));
adapter.addItem(new Item("냐냐", "010-1000-1000"));
adapter.addItem(new Item("니니", "010-2000-2000"));

// 내부 데이터가 변경되었음을 알림
adapter.notifyDataSetChanged()

recyclerView.setAdapter(adapter);

 

 

아이템 클릭 이벤트 추가하기

이렇게 하면 RecyclerView를 구현하는 것은 끝난다. 하지만 이렇게만 하면 아이템을 RecyclerView 형태로 볼 수 만 있기 때문에 사용자가 각 아이템을 클릭했을 때 발생하는 클릭 이벤트를 만들어 주어야 한다. 그 예제로 아이템을 클릭 하면 토스트 메시지를 띄우는 기능을 만들어 보자.

 

클릭 이벤트는 RecyclerView가 아니라 각 아이템에 발생하게 되므로 뷰홀더 안에서 클릭 이벤트를 처리할 수 있도록 만드는 것이 좋다. 뷰홀더의 생성자로 뷰 객체가 전달되므로 이 뷰 객체에 OnClickListener를 설정하게 하면, 이 뷰를 클릭했을 때 그 리스너의 onClick() 메서드가 호출된다. 그런데 이 리스너 안에서 토스트 메시지를 띄우게 되면 클릭했을 때의 기능이 변경될 때마다 어댑터를 수정해야 하는 문제가 생긴다. 어댑터 내부를 수정하는 것이 문제가 되는 이유는, 사용자 입장에서 어댑터는 안의 정확한 동작을 몰라도 명세만 보고 사용할 수 있어야 하는데 어댑터 내부를 수정하려면 어댑터 내부의 동작을 알아야 하기 때문이다. 또한 그렇게 하면 객체 간의 의존성이 높아지므로 피해야 하는 방법이다. 이러한 이유로 어댑터 객체 밖에서 리스너를 설정하고 설정된 리스너 쪽으로 이벤트를 전달 받도록 하는 것이 좋다. 그렇게 하면 사용자는 클릭 리스너의 동작을 정의하고 그 리스너를 어댑터에게 전달하기만 하면 되는 것이다.

 

이를 위해 OnItemClickListener 인터페이스를 정의한다. 이 인터페이스의 메서드인 onItemClick() 메서드는 호출될 때 파라미터로 뷰홀더 객체와 뷰 객체 그리고 해당 뷰가 몇 번째 아이템인지를 구분할 수 있는 인덱스 값인 position 정보가 전달되도록 한다. 

public interface OnItemClickListener {
    void onItemClick(Adapter.ViewHolder viewHolder, View view, int position);
}

 

이제 이 인터페이스를 사용하도록 ViewHolder 클래스를 수정한다.

static class ViewHolder extends RecyclerView.ViewHolder{
    ...

    // 각각의 item에 대한 뷰가 뷰홀더의 파라미터로 전달 됨.
    ViewHolder(View itemView, final OnItemClickListener listener) {
        super(itemView);

        textView = itemView.findViewById(R.id.textView);
        textView2 = itemView.findViewById(R.id.textView2);

        itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (listener != null) {
                    listener.onItemClick(ViewHolder.this, view, getAdapterPosition());
                }
            }
        });
    }

    ...
}

 

뷰홀더 객체의 생성자가 호출될 때 리스너 객체가 파라미터로 전달되도록 수정했다. 이 리스너 객체는 어댑터 밖에서 설정할 것이며 뷰홀더까지 전달된다. 이렇게 전달된 리스너 객체의 onItemClick 이벤트는 뷰가 클릭되었을 때 호출된다. 위 코드에서 getAdapterPosition() 메서드는 이 뷰홀더에 표시할 아이템이 어댑터에서 몇 번째인지 정보를 반환하는데, 그 값이 파라미터로 넘어가면서 리스너에 전달된다.

 

이제 ViewHolder가 수정되었으니 Adapter 클래스도 수정해 주어야 하는 부분이 있다.

public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> implements OnItemClickListener {
    private ArrayList<Item> items = new ArrayList<>();
    private OnItemClickListener listener;	// 리스너 객체 추가

    void addItem(Item item) { items.add(item); }
    Item getItem(int position) { return items.get(position); }	// 아이템 리턴하는 함수 추가

    void setOnItemClickListener(OnItemClickListener listener) {
        this.listener = listener;
    }

    @Override
    public void onItemClick(ViewHolder viewHolder, View view, int position) {
        if (listener != null) {
            listener.onItemClick(viewHolder, view, position);
        }
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View itemView = inflater.inflate(R.layout.item, parent, false);

        return new ViewHolder(itemView, this);    // 이 부분 수정
    }
    
    ...
    
    static class ViewHolder extends RecyclerView.ViewHolder{
        ...
    }
 
 }

 

먼저 Adapter 클래스는 새로 정의한 OnItemClickListener 인터페이스를 구현하도록 한다. 그리고 이 인터페이스에서 정의한 onItemClick() 메서드를 재정의 해야 하는데, 이 메서드는 아이템이 클릭되었을 때 호출되는 메서드로 이 안의 동작은 어댑터 클래스 안에서가 아니라 밖에서 처리하는 것이 일반적이다. 그렇기 때문에 listener라는 이름의 멤버 변수를 선언하고 setOnItemClickListener() 메서드를 추가하여 이 메서드가 호출되었을 때 리스너 객체를 변수에 할당하도록 한다. 이렇게 하면 onItemClick() 메서드가 호출되었을 때 다시 외부에서 설정된 메서드가 호출되도록 만들 수 있다.

마지막으로 onCreateViewHolder() 메서드 안에서 new 연산자를 이용해 ViewHolder 객체를 생성하는 코드를 수정해야 한다. 이전에는 뷰 객체만 파라미터로 전달했지만 여기에 리스너인 this를 추가로 전달해야 한다. Adapter가 OnItemClickListener 인터페이스를 구현하고 있기 때문에 this가 파라미터로 들어갈 수 있다.

 

이제 MainActivity의 어댑터에 리스너 객체를 설정하는 코드를 추가하자.

adapter.setOnItemClickListener(new OnItemClickListener() {
    @Override
    public void onItemClick(Adapter.ViewHolder viewHolder, View view, int position) {
        Item item = adapter.getItem(position);

        Toast.makeText(MainActivity.this, "이름: " + item.getName(), Toast.LENGTH_SHORT).show();
    }
});

 

이렇게 하면 외부에서 Adapter 객체를 만든 뒤  setOnItemClickListener() 메서드 안에 onItemClick()의 동작을 정의한 리스너를 넘겨주기만 하면 사용자가 정의한 대로 클릭 이벤트가 발생한다. 여기서 adapter 객체가 onItemClick() 메서드 내부에서 사용되므로 adapter 객체는 클래스 멤버 변수로 수정해 주어야 한다.

 

이렇게 하면 클릭 동작까지 하는 RecyclerView를 만들 수 있다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함