RecyclerView는 Android Jetpack의 구성요소
로 화면에 표시되지 않는 아이템 뷰를 재활용
하여 새로운 데이터를 표시하는 위젯입니다.
ListView
와 비슷하게 스크롤 가능한 목록을 보여주는 데 사용되지만, ListView
보다 더 유연하고 성능이 뛰어나다. 그 이유는 RecyclerView
가 뷰 홀더 패턴
을 내장하고 있기 때문이다. 뷰 홀더 패턴
이란, 뷰의 재사용을 가능하게 해주는 디자인 패턴으로, 아이템 뷰의 데이터를 일시적으로 저장하는 객체를 사용하여 뷰의 생성과 데이터의 설정을 분리한다. 이로 인해 스크롤 시 필요한 뷰를 빠르게 재사용하고 데이터를 설정할 수 있다.
이 패턴을 사용함으로써 RecyclerView는 스크롤이 부드럽고
, 뷰가 재사용되기 때문에 메모리를 효율적으로 사용
할 수 있다.
Adapter
는 데이터 리스트와 RecyclerView 사이의 통신
을 담당한다. Adapter 생성자에 데이터 리스트를 넘겨주거나, setDataList()를 통해 데이터를 넘겨준다. Adapter는 데이터를 가져온 뒤, ViewHolder를 생성하고 ViewHolder에 데이터를 바인딩
한다. 일반적으로 4가지 함수를 오버라이드 해서 구현한다.
onCreateViewHolder()
함수는 ViewHolder가 생성될 때 호출된다. 처음 RecyclerView가 생성되어 화면을 채우기 위해 아이템 뷰가 필요하거나, 스크롤 도중 새로운 타입의 아이템 뷰가 필요하게 될 때 호출된다.
이 함수안에서는 XML 레이아웃 파일을 inflate하고 그 결과로 생성된 View를 바탕으로 새 ViewHolder 인스턴스를 생성한다. 이렇게 생성된 ViewHolder는 RecyclerView의 뷰 풀(View Pool)에 저장된다.
onBindViewHolder()
함수는 RecyclerView가 ViewHolder에 데이터를 바인딩
해야 할 때 호출된다. 이 함수는 ViewHolder의 뷰에 특정 위치(position)에 해당하는 데이터를 바인딩한다. 만약 뷰 풀에 이미 존재하는 ViewHolder가 재사용되는 경우라면, 이 함수에서 이전에 설정되었던 데이터를 새 데이터로 교체해서 사용한다.
getItemCount()
함수는 RecyclerView에 표시될 아이템의 총 개수
를 반환한다.
getItemViewType()
함수는 position에 따른 뷰 타입을 반환한다. 예를 들어 채팅 앱을 만든다면 내가 보낸 채팅 뷰는 1, 상대가 보낸 채팅 뷰는 2로 설정하면 onCreateViewHolder() 함수 안에서 viewType 값을 받아와 뷰를 다르게 생성하고 뷰 풀에 저장한다.
해당 함수를 구현하지 않으면 모든 데이터 항목이 같은 타입의 뷰를 사용한다고 가정하고 항상 0을 반환한다.
ViewHolder
는 리사이클러 뷰 아이템의 뷰를 저장하는 컨테이너이다. 이는 findViewById() 호출을 최소화하여 성능을 더욱 향상시킨다. onCreateViewHolder() 함수에 의해서 생성되며 RecyclerView의 뷰 풀에 저장된다.
LayoutManager
는 아이템 뷰의 레이아웃을 관리한다. 예를 들어, 아이템을 세로 리스트로 표시할지, 가로 리스트로 표시할지, 그리드 형식으로 표시할지 등을 결정한다. LinearLayoutManager
, GridLayoutManager
, StaggeredGridLayoutManager
등 다양한 유형의 레이아웃 매니저를 지원한다.
<?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>
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
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)
}
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
}
}
ListView
: getView() 함수를 사용하여 뷰를 생성하고 재사용 하는데, 이 과정에서 findViewById() 함수를 사용하여 UI 요소를 찾아야 하며, 이때 CPU 리소스를 많이 사용한다.RecyclerView
: 뷰 홀더 패턴
을 사용하여 뷰의 재사용을 효율적으로 관리하며, 뷰의 레이아웃 요소를 저장하고 재사용하므로 findViewById()를 매번 호출할 필요가 없다.
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와 메모리 사용량을 효율적으로 관리
한다.
ListView
: 단순한 선형 리스트만 지원RecyclerView
: LayoutManager를 사용하여 다양한 레이아웃을 구현 가능ListView
: 개발자 능력을 많이 요구RecyclerView
: 아이템 데코레이션
과 애니메이션
을 쉽게 추가할 수 있어 확장성이 뛰어남ListView, RecyclerView에 각각 아이템 10만개 추가하고 스크롤 하여 메모리 사용량 비교 약 41 MB 차이남
ListView 메모리 사용량 Total: 154.4 MV
RecyclerView 메모리 사용량 Total: 113.5 MV