[Android] RecyclerView와 ListAdapter (Java)

th.k·2021년 1월 9일
1

Android

목록 보기
1/8

(공식)RecyclerView
(공식)RecyclerView 릴리즈 노트
(공식)ListAdapter

ListAdapter

ListAdapter는 RecyclerView에 쓸 수 있는 어댑터이다.
기존의 어댑터와는 다르게 DiffUtil을 사용하여 비동기식 처리를 할 수 있다.
기존의 기본 어댑터는 nofity~() 류의 메소드를 사용하여 리스트의 변경처리를 했었는데, ListAdapter에서는 submitList(...) 를 사용하여 리스트가 변경되었음을 어댑터에게 알려줄 수 있다.

DiffUtil

ListAdapter가 기존의 리스트와 새로운 리스트를 비교할 때 쓰는 연산이다.
빠른 속도로 비교 연산을 해준다는데 구체적인 수치같은건 까먹었다...
기존의 리스트와 새로운 리스트의 각각의 아이템을 비교해서 같은 아이템인지 아닌지 비교하는데, 어떤 내용을 비교할지 지정할 수 있다.
비교 결과, 다르다고 판단된 아이템 항목만 교체를 진행한다. 이것이 중요한 포인트이다.
원래는 상황에 맞게 몇번 위치의 아이템이 지워졌는지, 추가됐는지, 내용이 바뀌였는지를 다르게 처리해줘야 했지만... 이건 그러지 않아도 된다.

주의 : 기존의 리스트와 새로운 리스트를 비교해야 하기 때문에, 두개의 리스트가 실제적으로 다른 리스트여야 한다. 같은 리스트를 참조하면 안된다.

구현

메인 화면에 리사이클러뷰가 있고, 리사이클러뷰에는 숫자가 적혀있는 항목들이 나타나게 만들었다.
그리고 상단에 버튼을 둬서, 누르면 0부터 10까지의 랜덤한 숫자가 적혀있는 리스트가 갱신되게 만들었다.

레이아웃 구현 전에 리사이클러뷰를 gradle(App)에 추가하지 않았다면 추가하자...

dependencies {
    // 버전은 릴리즈 노트에서 참고
    implementation "androidx.recyclerview:recyclerview:1.1.0"
}

레이아웃

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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"
    android:background="@color/white"
    >

    <!-- 리스트를 새로고침 하기위한 버튼 -->
    <Button
        android:id="@+id/btn_refresh"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="refresh"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        />

    <!-- 리사이클러 뷰 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/btn_refresh"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

constraintLayout을 이용해서 구성하였다.
화면 맨 위에는 리스트를 새로고침 할 버튼, 그 밑에는 화면을 꽉 채우는 리사이클러 뷰를 배치했다.

item_list_unit.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:padding="8dp"
    android:background="@color/white"
    >

    <!-- 심심하니까 넣은 아이콘 이미지 -->
    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="@mipmap/ic_launcher"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        />

    <!-- 숫자 String이 보여질 텍스트뷰 -->
    <TextView
        android:id="@+id/tv_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:textSize="20dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/iv_icon"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

리사이클러뷰에 나타날 아이템의 레이아웃이다.
ConstraintLayout을 사용했고, 가장 왼쪽엔 안드로이드 스튜디오에서 제공해주는 아이콘과 그 옆에는 숫자 String이 나타날 TextView를 배치했다.

최종 레이아웃


화면구성 끝

코드

먼저 ListAdapter를 만들자

MyListAdapter.java

MyListAdapter라는 이름의 클래스를 만들어 주었다.
그리고 ListAdapter<T, VH> 를 상속받아준다.

public class MyListAdapter extends ListAdapter<String, MyListAdapter.MyViewHolder> {

    // 클래스 내부... 
}

ListAdapter<T, VH>에서 T는 리사이클러뷰가 화면에 표시할 데이터의 타입이다. 나는 숫자 String을 표현하기때문에 단순히 String을 지정해줬지만, 직접 만든 클래스 타입을 사용할 수 있다.
VHViewHolder이다. 아직 만들지 않은 MyViewHolder라는 클래스를 써줬다.

상속받으면 에러가 뜨는데, 있어야 할 생성자와 ViewHolder와 필수 구현 메소드들이 없어서 그렇다

1.생성자 구현하기

control+O를 누르면 자동완성 리스트가 뜨는데, 상단의 ListAdapter(diffCallback:ItemCallback<T>)를 선택한다.

protected MyListAdapter(@NonNull DiffUtil.ItemCallback<String> diffCallback) {
    super(diffCallback);
}

public이 아닌 protected로 생성이 되는데... 문제가 된다면 public으로 바꿀 수 있다.

주의 : 다른 생성자인 ListAdapter(config:AsyncDifferConfig<T>)랑 헷갈리면 안된다.

2. ViewHolder 만들기

내부 클래스로 ViewHolder를 상속받는 MyViewHolder를 만들어준다.

public static class MyViewHolder extends RecyclerView.ViewHolder {

    // item_list_unit.xml에 있는 텍스트뷰
    private TextView tv_number;

    // 뷰홀더의 생성자
    public MyViewHolder(@NonNull View itemView) {
        super(itemView);

        // 여기서 초기화 진행
        tv_number = itemView.findViewById(R.id.tv_number);
    }

    // 레이아웃과 데이터를 연결하는 메소드
    private void bind(String strNum) {
        tv_number.setText(strNum);
    }
}

이건 기존의 어댑터랑 같다.

3. 필수 구현 메소드

기존 어댑터랑 마찬가지로 onCreateViewHolder(...)onBindViewHolder(...)를 구현한다.

@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext())
    			.inflate(R.layout.item_list_unit, parent, false);

    return new MyViewHolder(view);
}

@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
    holder.bind(getItem(position));
}

onBindViewHolder(...)에 레이아웃에 데이터를 바인드해주는 코드를 작성하지 않고 뷰홀더 쪽에 bind(String strNum)라는 메소드로 작성했다.
여기서 bind(...) 메소드에 넘겨줄 데이터를 가져오는 방식이 기존의 어댑터와 달라진다.
원래는 어댑터 클래스에 데이터를 가지고있는 ArrayList<T> dataList 같은 리스트가 전역변수로 있고, dataList.get(position)으로 데이터를 가져왔던 것 같은데, ListAdapter에서는 그냥 getItem(int position)이라는 메소드를 사용하면 된다.

간단하게 ListAdapter를 구현했다.

전체 코드 :

public class MyListAdapter extends ListAdapter<String, MyListAdapter.MyViewHolder> {

    // ListAdapter의 생성자
    protected MyListAdapter(@NonNull DiffUtil.ItemCallback<String> diffCallback) {
        super(diffCallback);
    }

    // 뷰홀더 클래스
    public static class MyViewHolder extends RecyclerView.ViewHolder {

        private TextView tv_number;

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

            tv_number = itemView.findViewById(R.id.tv_number);
        }

        private void bind(String strNum) {
            tv_number.setText(strNum);
        }
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_unit, parent, false);

        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        holder.bind(getItem(position));
    }
}

MyDiffUtil.java

ListAdapter가 사용할 DiffUtil을 만들어보자.
MyDiffUtil이라는 이름으로 클래스를 만들었다.

public class MyDiffUtil extends DiffUtil.ItemCallback<String> {
    @Override
    public boolean areItemsTheSame(@NonNull String oldItem, @NonNull String newItem) {
        return oldItem.equals(newItem);
    }

    @Override
    public boolean areContentsTheSame(@NonNull String oldItem, @NonNull String newItem) {
        return oldItem.equals(newItem);
    }
}

DiffUtil.ItemCallback<T>를 상속받아주면 된다.
마찬가지로 <T>에는 화면에 표시할 데이터의 타입을 써주면된다.

필수로 구현해야 하는 메소드로 areItemsTheSame(...)areContentsTheSame(...)이 있다. 이 둘은 기존의 리스트와 새로운 리스트의 각 항목을 순서대로 비교한다.

areItemsTheSame(...)

아이템 항목 자체가 같은지 비교한다.
코드는 데이터 타입이 String이라서 단순히 문자열이 같은지만 비교하지만, 커스텀 클래스 타입을 사용한다면 각 항목이 가지는 고유한 값을 비교하여 항목이 같은지 아닌지를 판별할 수 있다.
예를들어, 이름과 전화번호를 보여주는 리스트라면, 이름이 고유한 값이 될 것이다. 이름이 같다면 true를, 다르다면 false을 반환한다.
false인 경우, 아이템 자체가 달라졌기 때문에 그 내용을 비교할 것도 없이 새로운 리스트의 아이템으로 교체한다.
true인 경우, 아이템 자체는 같지만 그 내용이 달라졌을지도 모르기 때문에 areContentsTheSame(...)를 호출한다.

areContentsTheSame(...)

아이템의 내용을 비교한다.
아이템의 어떤 내용을 비교할 것인지는 알아서 작성하면 된다.
전화번호부 리스트라면, 이름은 같지만 전화번호가 달라졌는지 비교하면 될 것이다. 번호가 여전히 같으면 true를, 달라졌다면 false를 반환한다.
false의 경우, 내용이 달라졌기 때문에 새로운 리스트의 아이템으로 교체한다.
true의 경우, 기존 리스트의 아이템과 새로운 리스트의 아이템이 완전히 일치하다고 보고, 아무일도 일어나지 않는다.

DiffUtil 구현 끝

MainActivity.java

만든 ListAdapter랑 DiffUtil을 붙여보자.

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private MyListAdapter myListAdapter;

    private Button btn_refresh;

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

        // DiffUtil을 넣은 어댑터를 생성
        myListAdapter = new MyListAdapter(new MyDiffUtil());

        recyclerView = findViewById(R.id.recyclerView);
        // recyclerView의 리스트 형태를 세로 목록형으로 지정
        recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false));
        // recyclerView에 어댑터 설정
        recyclerView.setAdapter(myListAdapter);

        btn_refresh = findViewById(R.id.btn_refresh);
        btn_refresh.setOnClickListener(v -> {
            // 크기가 5개인 새로운 리스트 생성
            List<String> newList = generateStringList(5);

            // 새로운 리스트를 넘겨준다. 비동기처리로 화면을 갱신해줌
            myListAdapter.submitList(newList);
        });
    }

    /**
     * 랜덤한 숫자 String을 가지는 List를 만드는 메소드
     * @param size List의 크기
     * @return 숫자 String을 가지는 List
     */
    private List<String> generateStringList(int size) {
        ArrayList<String> arrList = new ArrayList<>();

        for (int i = 0; i < size; ++i) {
            int num = (int)(Math.random() * 10);
            arrList.add(String.valueOf(num));
        }

        return arrList;
    }
}

귀찮아서 통으로 넣겠다...

리사이클러뷰에 레이아웃 매니저 설정하고, 어댑터 설정하는 것도 다 같지만
어댑터를 생성할 때 MyDiffUtil의 인스턴스를 넣어준다.

btn_refresh에는 generateStringList(int size)를 사용해서 새로운 리스트를 얻어오고, 어댑터의 submitList(List<T> newList)메소드를 사용해서 새로운 리스트를 넣어주는 기능을 구현해줬다.

앱을 실행해보면 상단의 refresh 버튼을 누를 때 마다 리스트가 갱신되는데, 기존의 리스트와 새로운 리스트의 값이 같은 항목들을 제외하고 다른 값을 가지는 항목들만 교체된다.

리스트가 갱신이 완료되는 시점을 알고싶은데 submitList(...) 메소드가 비동기식이라서 언제 갱신이 끝나는지 알 수 없다.
그렇다면 submitList(List<T> newList, Runnable r)을 사용해서 Runnable을 넘겨줄 수 있다.

맨 처음에 ListAdapter의 존재를 알았을 때 써보고 싶어서 방법을 찾아봤지만 대부분 코틀린으로 구현한 것 밖에 안보였고, 코드가 조각조각나 있어서 눈에 잘 안들어왔던 것 같다. 아마도... 그냥 내가 이해를 못한걸수도ㅎㅎ
그래서 그냥 자바로 정리해두고 싶었다.

profile
고생끝에롹이온다

2개의 댓글

comment-user-thumbnail
2022년 9월 22일

Java 로 된 자료는 정말 찾기 힘들었는데 귀한 자료 공개 해 주셔서 너무 감사합니다

1개의 답글