RecyclerView

강현성·2023년 8월 5일
0

android

목록 보기
18/18

1. RecyclerView

RecyclerView는 Android Jetpack의 구성요소로 화면에 표시되지 않는 아이템 뷰를 재활용하여 새로운 데이터를 표시하는 위젯입니다.

ListView와 비슷하게 스크롤 가능한 목록을 보여주는 데 사용되지만, ListView보다 더 유연하고 성능이 뛰어나다. 그 이유는 RecyclerView뷰 홀더 패턴을 내장하고 있기 때문이다. 뷰 홀더 패턴이란, 뷰의 재사용을 가능하게 해주는 디자인 패턴으로, 아이템 뷰의 데이터를 일시적으로 저장하는 객체를 사용하여 뷰의 생성과 데이터의 설정을 분리한다. 이로 인해 스크롤 시 필요한 뷰를 빠르게 재사용하고 데이터를 설정할 수 있다. 이 패턴을 사용함으로써 RecyclerView는 스크롤이 부드럽고, 뷰가 재사용되기 때문에 메모리를 효율적으로 사용할 수 있다.

2. 주요 구성 요소

2-1. Adapter

Adapter데이터 리스트RecyclerView 사이의 통신을 담당한다. Adapter 생성자에 데이터 리스트를 넘겨주거나, setDataList()를 통해 데이터를 넘겨준다. Adapter는 데이터를 가져온 뒤, ViewHolder를 생성하고 ViewHolder에 데이터를 바인딩한다. 일반적으로 4가지 함수를 오버라이드 해서 구현한다.

2-1-1. onCreateViewHolder(ViewGroup parent, int viewType)

onCreateViewHolder() 함수는 ViewHolder가 생성될 때 호출된다. 처음 RecyclerView가 생성되어 화면을 채우기 위해 아이템 뷰가 필요하거나, 스크롤 도중 새로운 타입의 아이템 뷰가 필요하게 될 때 호출된다. 이 함수안에서는 XML 레이아웃 파일을 inflate하고 그 결과로 생성된 View를 바탕으로 새 ViewHolder 인스턴스를 생성한다. 이렇게 생성된 ViewHolder는 RecyclerView의 뷰 풀(View Pool)에 저장된다.

2-1-2. onBindViewHolder(ViewHolder holder, int position)

onBindViewHolder() 함수는 RecyclerView가 ViewHolder에 데이터를 바인딩해야 할 때 호출된다. 이 함수는 ViewHolder의 뷰에 특정 위치(position)에 해당하는 데이터를 바인딩한다. 만약 뷰 풀에 이미 존재하는 ViewHolder가 재사용되는 경우라면, 이 함수에서 이전에 설정되었던 데이터를 새 데이터로 교체해서 사용한다.

2-1-3. getItemCount()

getItemCount() 함수는 RecyclerView에 표시될 아이템의 총 개수를 반환한다.

2-1-4. getItemViewType(int position)

getItemViewType()함수는 position에 따른 뷰 타입을 반환한다. 예를 들어 채팅 앱을 만든다면 내가 보낸 채팅 뷰는 1, 상대가 보낸 채팅 뷰는 2로 설정하면 onCreateViewHolder() 함수 안에서 viewType 값을 받아와 뷰를 다르게 생성하고 뷰 풀에 저장한다. 해당 함수를 구현하지 않으면 모든 데이터 항목이 같은 타입의 뷰를 사용한다고 가정하고 항상 0을 반환한다.

2-2. ViewHolder

ViewHolder리사이클러 뷰 아이템의 뷰를 저장하는 컨테이너이다. 이는 findViewById() 호출을 최소화하여 성능을 더욱 향상시킨다. onCreateViewHolder() 함수에 의해서 생성되며 RecyclerView의 뷰 풀에 저장된다.

2-3. LayoutManager

LayoutManager는 아이템 뷰의 레이아웃을 관리한다. 예를 들어, 아이템을 세로 리스트로 표시할지, 가로 리스트로 표시할지, 그리드 형식으로 표시할지 등을 결정한다. LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager 등 다양한 유형의 레이아웃 매니저를 지원한다.

3. RecyclerView 구현

3-1. item.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="viewmodel"
            type="com.youngcha.ohmycarset.viewmodel.TrimSelectViewModel" />

        <variable
            name="trim"
            type="com.youngcha.ohmycarset.model.Trim" />
    </data>

    <androidx.cardview.widget.CardView
        android:layout_width="200dp"
        android:layout_height="85dp"
        android:layout_marginStart="12dp"
        app:cardBackgroundColor="@{trim.isChecked ? @color/main_hyundai_blue : @color/cool_grey_001}"
        app:cardCornerRadius="6dp">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:onClick="@{() -> viewmodel.onItemClicked(trim)}">

            <TextView
                android:id="@+id/tv_trim_explain"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="20dp"
                android:layout_marginTop="20dp"
                android:fontFamily="@font/hyundai_sans_text_kr_regular"
                android:text="@{trim.explain}"
                android:textColor="@{trim.isChecked ? @color/white : @color/cool_grey_003}"
                android:textSize="12sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="#베스트 셀러" />

            <TextView
                android:id="@+id/tv_trim_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:fontFamily="@font/hyundai_sans_text_kr_medium"
                android:text="@{trim.name}"
                android:textColor="@{trim.isChecked ? @color/white : @color/cool_grey_003}"
                android:textSize="20sp"
                app:layout_constraintStart_toStartOf="@id/tv_trim_explain"
                app:layout_constraintTop_toBottomOf="@id/tv_trim_explain"
                tools:text="Le Blanc" />

            <ImageView
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_marginTop="20dp"
                android:layout_marginEnd="14dp"
                android:src="@{trim.isChecked ? @drawable/ic_check : @drawable/ic_check_off}"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>

</layout>

3-2. Data class 구현

package com.youngcha.ohmycarset.model

import android.os.Parcelable
import com.youngcha.ohmycarset.enums.TrimType
import kotlinx.parcelize.Parcelize

@Parcelize
data class Trim(
    val type: TrimType,
    val name: String,
    val explain: String,
    var isChecked: Boolean
): Parcelable

3-3. Adapter & ViewHolder 구현

package com.youngcha.ohmycarset.ui.adapter

import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.youngcha.ohmycarset.databinding.ItemTrimSelectBinding
import com.youngcha.ohmycarset.model.Trim
import com.youngcha.ohmycarset.viewmodel.TrimSelectViewModel

class TrimSelectAdapter(
    private var trims: List<Trim>,
    private val viewModel: TrimSelectViewModel
) : RecyclerView.Adapter<TrimSelectAdapter.TrimSelectViewHolder>() {

    @SuppressLint("NotifyDataSetChanged")
    fun updateTrims(trims: List<Trim>) {
        this.trims = trims
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrimSelectViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = ItemTrimSelectBinding.inflate(inflater, parent, false)
        return TrimSelectViewHolder(binding)
    }

    override fun onBindViewHolder(holder: TrimSelectViewHolder, position: Int) {
        val trim = trims[position]
        holder.binding.trim = trim
        holder.binding.viewmodel = viewModel
        holder.binding.executePendingBindings()
    }

    override fun getItemCount(): Int = trims.size

    class TrimSelectViewHolder(val binding: ItemTrimSelectBinding) :
        RecyclerView.ViewHolder(binding.root)
}

3-4. 사용


class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: TrimSelectViewModel by viewModels()
    private lateinit var adapter: TrimSelectAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        init()
    }

    private fun init() {
        val trims = listOf(
            Trim(TrimType.GUIDE, "#Guide Mode", "#나만을 위한 팰리세이드", true),
            Trim(TrimType.SELF, "Exclusive", "#기본에 충실", false),
            Trim(TrimType.SELF, "Le Blanc", "#베스트셀러", false),
            Trim(TrimType.SELF, "Prestige", "#부담없는 고급감", false),
            Trim(TrimType.SELF, "Calligraphy", "#최고를 원한다면", false)
        )

        adapter = TrimSelectAdapter(trims, viewModel)

        viewModel.setTrims(trims)

        binding.rvTrimSelect.apply {
            layoutManager =
                LinearLayoutManager(this@MainActivity, LinearLayoutManager.HORIZONTAL, false)
            adapter = this@MainActivity.adapter
        }

    }

4. RecyclerView Flow

1️⃣ RecyclerView의 Adapter는 표시해야 할 데이터 리스트를 전달 받는다.

2️⃣ RecyclerView가 처음 화면에 표시될 때, onCreateViewHolder() 함수가 호출되어 필요한 수의 ViewHolder 객체가 생성된다.(화면 갯수) 이 객체들은 각각의 아이템 뷰를 담당하게 된다.

3️⃣ 사용자가 RecyclerView를 스크롤하면, 화면 밖으로 벗어나는 아이템의 ViewHolder는 뷰 풀에 저장된다. 이 ViewHolder는 이제 화면에 보이지 않으므로, 새로 보여질 아이템에 재사용될 수 있다.

4️⃣ RecyclerView가 새로운 아이템을 화면에 표시해야 할 때, 먼저 뷰 풀을 확인한다. 같은 타입의 ViewHolder가 뷰 풀에 있으면, RecyclerView는 이를 재사용하여 새 아이템을 표시한다. 이 과정에서 onBindViewHolder() 함수가 호출되어, ViewHolder의 뷰에 새 데이터를 바인딩한다.

5️⃣ 만약 뷰 풀에 필요한 타입의 ViewHolder가 없다면, RecyclerView는 onCreateViewHolder()를 호출하여 새로운 ViewHolder를 생성한다.

이렇게 RecyclerView는 뷰 풀을 통해 뷰의 재사용을 관리한다. 따라서 사용자가 빠르게 스크롤하거나, 표시할 아이템의 수가 많아도 RecyclerView는 부드럽게 스크롤된다.

5. RecyclerView와 ListView의 차이점

5-1. 뷰 재사용

  • ListView: getView() 함수를 사용하여 뷰를 생성하고 재사용 하는데, 이 과정에서 findViewById() 함수를 사용하여 UI 요소를 찾아야 하며, 이때 CPU 리소스를 많이 사용한다.
  • RecyclerView: 뷰 홀더 패턴을 사용하여 뷰의 재사용을 효율적으로 관리하며, 뷰의 레이아웃 요소를 저장하고 재사용하므로 findViewById()를 매번 호출할 필요가 없다.

5-1-1. ListView와 메모리 사용

ListView의 getView() 함수는 BaseAdapter 또는 ArrayAdapter와 같은 Adapter 클래스를 오버라이드 해서 사용하는 함수이다.

다음은 getView()를 오버라이드해서 구현한 코드이다.

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
    }
    TextView textView = convertView.findViewById(R.id.text_view);
    textView.setText(dataList.get(position));
    return convertView;
}

getView() 함수는 ListView에서 아이템 뷰를 생성하거나 재사용하는 역할을 한다. convertView 매개변수는 이전에 사용한 뷰를 가리키는데, 이 값이 null이 아니라면 해당 뷰를 재사용하고, null이라면 새로운 뷰를 생성한다. 뷰를 생성하는 경우 LayoutInflater를 사용하여 XML 레이아웃 파일을 로드하게 되면서 비용이 발생한다. 여기서 생성하는 뷰의 수가 많아지면 메모리 사용량이 상승하게 된다. 또한, getView()에서 뷰를 생성하거나 재사용한 후에는 해당 뷰에 데이터를 설정해야 한다. 이 때 일반적으로 findViewById()를 호출하여 데이터를 설정할 뷰를 찾게 되는데, 이때도 비용이 발생한다. 즉, ListView는 스크롤될 때마다 getView() 함수를 호출하여 아이템 뷰를 생성하거나 재사용하고 이 과정에서 뷰가 재사용된다고 해도 findViewById() 함수가 호출되기 때문에 비용이 발생하게 된다. 따라서 ListView에서는 getView()의 호출로 인해 뷰 생성과 findViewById() 호출이 빈번하게 이루어지므로, 이로 인한 CPU 사용량과 메모리 사용량이 늘어날 수 있다. 이를 해결하기 위해 RecyclerView에서는 뷰 홀더 패턴을 사용하여 뷰와 그에 대한 참조를 재사용함으로써, findViewById() 호출을 최소화하고 CPU와 메모리 사용량을 효율적으로 관리한다.

5-2. 레이아웃 관리

  • ListView: 단순한 선형 리스트만 지원
  • RecyclerView: LayoutManager를 사용하여 다양한 레이아웃을 구현 가능

5-3. 애니메이션과 데코레이션

  • ListView: 개발자 능력을 많이 요구
  • RecyclerView: 아이템 데코레이션애니메이션을 쉽게 추가할 수 있어 확장성이 뛰어남

6. Android Studio Profiler 사용하여 ListView, RecyclerView 메모리 사용량 비교

ListView, RecyclerView에 각각 아이템 10만개 추가하고 스크롤 하여 메모리 사용량 비교 약 41 MB 차이남

  • ListView 메모리 사용량 Total: 154.4 MV

  • RecyclerView 메모리 사용량 Total: 113.5 MV

참고 자료

RecyclerView 공식 문서

RecyclerView View Pool 공식 문서

Profiler 사용하여 메모리 사용량 검사하기 공식 문서

profile
Hello!

0개의 댓글