안드로이드 RecyclerView

강현성·2022년 8월 9일
0

android

목록 보기
3/18

1. RecyclerView

RecyclerView는 데이터를 목록 형태로 보여줘 스크롤이 가능하도록 한 컨테이너이다.

기존에 데이터를 목록 형태로 보여줘 사용자가 스크롤 할 수 있도록 하기 위해서는 ListView를 사용했다. ListView는 각 아이템이 생성될 때 매번 뷰 바인딩을 하므로 성능이 떨어지는 문제가 있었다. 반면, RecyclerView는 ViewHolder 패턴을 강제로 구현하도록 하여 초기에 뷰 바인딩 한 번이면 이후 아이템이 생성될 때 기존에 바인딩 된 뷰 객체를 재사용하여 ListView에서 나타나는 성능 문제를 보완했다.

위 그림과 같이 ListView는 사용자가 스크롤을 하면 사용자가 보여지지 않는 부분의 뷰는 해제되고 새롭게 사용자에게 보여지는 부분이 매번 생성되어 성능 저하를 만들었다.

반면, RecyclerView는 화면에 꽉 차는만큼의 뷰를 만든 다음 사용자가 스크롤을 하여 사용자에게 보여지지 않는 부분의 뷰가 생기면 삭제하지 않고 사용자에게 새롭게 보여지는 부분의 뷰로 재사용한다는 장점이 있다.

또한, RecyclerView는 여러 방향의 레이아웃 형태를 지원한다. 다만 RecyclerView에는 Click Listener가 존재하지 않으므로 직접 구현하여 사용해야 한다.

2. RecyclerView 구현 단계

2-1. 레이아웃 계획

RecyclerView의 항목은 LayoutManager 클래스를 통해 관리된다.

  • LinearLayoutManager: 1차원 목록으로 정렬
  • GridLayoutManager: 2차원 목록으로 정렬

2-2. 어댑터 및 뷰 홀더 구현

레이아웃을 결정했으면 Adapter 및 ViewHolder를 구현한다. 이 두 클래스가 함께 동작하여 데이터 표시 방식을 정의한다.

Adapter에 필요한 메서드

  • onCreateViewHolder(): RecyclerView는 ViewHolder를 새로 만들어야 할 때 이 메서드를 호출한다. ViewHolder와 연결된 View를 생성하고 초기화하는 부분
    // Create new views (invoked by the layout manager)
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        // Create a new view, which defines the UI of the list item
        View view = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.text_row_item, viewGroup, false);

        return new ViewHolder(view);
    }
  • onBindViewHolder(): RecyclerView는 ViewHolder를 데이터와 연결할 때 이 메서드를 호출한다. 예를들어 목록의 레이아웃이 TextView에 사용자의 이름을 표시한다면 이 TextView에 값을 이 메서드에서 할당해준다.
    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(ViewHolder viewHolder, final int position) {

        // Get element from your dataset at this position and replace the
        // contents of the view with that element
        viewHolder.getTextView().setText(localDataSet[position]);
    }
  • getItemCount(): RecyclerView에 총 표시되야 할 데이터 수
    // Return the size of your dataset (invoked by the layout manager)
    @Override
    public int getItemCount() {
        return localDataSet.length;
    }

3. RecyclerView 구현

📝 User.class(목록에 표시할 데이터)

class User {
    private String name;
    private String number;

    public User(String name, String number) {
        this.name = name;
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public String getNumber() {
        return number;
    }
}

📝 myadapter.xml(각 항목의 레이아웃)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_centerInParent="true">

        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#000000"
            android:text="홍길동"
            android:textSize="20dp"/>

        <TextView
            android:id="@+id/number"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FF0000"
            android:layout_toRightOf="@+id/name"
            android:text="1"
            android:textSize="15dp"
            android:layout_marginLeft="10dp"/>
    </LinearLayout>


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#CDCDCD"
        android:layout_alignParentBottom="true"/>
</RelativeLayout>

📝 MyAdapter.class

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private List<User> userList;
    private onItemClickListener listener;

    class MyViewHolder extends RecyclerView.ViewHolder {
        private TextView name;
        private TextView number;

        public MyViewHolder(View itemView) {
            super(itemView);
            name = itemView.findViewById(R.id.name);
            number = itemView.findViewById(R.id.number);

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    int position = getAdapterPosition();
                    if (listener != null && position != RecyclerView.NO_POSITION) {
                        listener.onItemClick(userList.get(position));
                    }
                }
            });
        }
    }

    public interface onItemClickListener {
        void onItemClick(User user);
    }

    public void setOnItemClickListener(onItemClickListener listener) {
        this.listener = listener;
    }

    public MyAdapter(List<User> userList) {
        this.userList = userList;
    }

    @NonNull
    @NotNull
    @Override
    public MyAdapter.MyViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.myadapter, parent, false);
        return new MyViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull @NotNull MyAdapter.MyViewHolder holder, int position) {
        User currentUser = userList.get(position);
        holder.name.setText(currentUser.getName());
        holder.number.setText(currentUser.getNumber());
    }

    @Override
    public int getItemCount() {
        return userList == null ? 0 : userList.size();
    }
}

📝 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

</RelativeLayout>

📝 MainActivity.class

public class MainActivity extends AppCompatActivity {
    
    RecyclerView recyclerView;
    MyAdapter myAdapter;
    RecyclerView.LayoutManager layoutManager;

    List<User> userList;

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

        userList = new ArrayList<>();

        for (int i = 1; i <= 20; i++) {
            userList.add(new User("홍길동", String.valueOf(i)));
        }

        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setHasFixedSize(true);

        myAdapter = new MyAdapter(userList);
        myAdapter.setOnItemClickListener(new MyAdapter.onItemClickListener() {
            @Override
            public void onItemClick(User user) {
                Toast.makeText(MainActivity.this, user.getNumber(), Toast.LENGTH_SHORT).show();
            }
        });
        recyclerView.setAdapter(myAdapter);
        
    }
}

🧨 RecyclerView 사용시 주의점 (항목 순서가 뒤죽박죽처럼 보이는 문제) 🧨

RecyclerView를 사용하다 보면 가끔 항목의 순서가 뒤죽박죽 섞일 때가 있다. 이 문제는 재사용되는 뷰가 초기화 되지 않아서 발생하는 문제이다. RecyclerView는 스크롤로 인해 보여지지 않는 뷰를 새롭게 보여져야 하는 뷰로 재사용하여 표시한다. 이때 뷰는 이미 바인딩이 되어 있다. 즉 이전 뷰를 재사용하는데 이전 뷰를 깨끗이 초기화 하지 않는다면 이전 뷰의 흔적이 남아 있게 되어 내가 원하는 뷰를 형태를 띄지 못해 순서가 뒤죽박죽인 RecyclerView가 나타나게 된다.

예를 들어 블로그 앱을 만든다고 한다면 홈 화면에는 모든 유저가 작성한 블로그들이 있을 것이다. 그리고 내가 작성한 글은 홈 화면에서 바로 삭제할 수 있도록 내가 작성한 글 한쪽에는 X 버튼이 보이도록 코드를 작성한다 하면 Adapter.class의 onBindViewHolder() 메서드 안의 코드는 아래와 같이 작성할 수 있다.

    @Override
    public void onBindViewHolder(@NonNull @NotNull MyAdapter.MyViewHolder holder, int position) {
        User currentUser = userList.get(position);
        String currentUserName = currentUser.getName();
        // 현재 view 항목에 표시되는 유저의 이름과 내 이름이 같다면 deleteImage를 표시
        if (currentUserName.equals(myName)) {
            holder.deleteImage.setVisibility(View.VISIBLE);
        }
        holder.name.setText(currentUser.getName());
        holder.number.setText(currentUser.getNumber());
    }

하지만, 위와 같이 onBindViewHolder() 메서드를 작성한다면 내가 원하는 목록 형태가 나오지 않는다. 현재 폰 화면에는 view1 ~ view10 까지가 보인다고 치자, view1은 내가 작성한 글이라 deletImage가 표시된다. 여기까지는 문제가 없지만 만약 스크롤을 해서 view11로 넘어가야 하고 이 view11은 내가 작성한 글이 아니다. 그래서 deleteImage는 표시되면 안된다. 하지만 recyclerView는 view1를 재사용하여 view11를 표시하기 때문에 deleteImage가 표시된 view1이 view11로 재사용 되어 내가 작성한 글이 아닌데도 view11에는 deleteImage가 표시된다. 이러한 문제로 인해 recyclerView의 순서가 뒤죽박죽처럼 보일때가 있다.

이 문제를 해결하기 위해서는 특성 상황에 따라 각 view 항목을 다르게 표시해야 될 때는 onBindViewHolder()메서드 맨 처음 부분에서 view를 초기화 해주는 방법이 있다.

    @Override
    public void onBindViewHolder(@NonNull @NotNull MyAdapter.MyViewHolder holder, int position) {
    	// onBindViewHolder() 메서드가 실행될 때 특정 상황에 따라 변해야 하는 component를 default 값으로 초기화
    	holder.deleteImage.setVisibility(View.GONE);
        User currentUser = userList.get(position);
        String currentUserName = currentUser.getName();
        // 현재 view 항목에 표시되는 유저의 이름과 내 이름이 같다면 deleteImage를 표시
        if (currentUserName.equals(myName)) {
            holder.deleteImage.setVisibility(View.VISIBLE);
        }
        holder.name.setText(currentUser.getName());
        holder.number.setText(currentUser.getNumber());
    }

위와 같이 onBindViewHolder() 메서드가 실행될 때 deleteImage의 default값으로 deleteImage를 보이지 않게 초기화 해주면 순서가 뒤죽박죽처럼 보이는 문제는 사라진다.

profile
Hello!

0개의 댓글