안드로이드 프로젝트를 몇 번 진행해보면서 리사이클러뷰가 정말 많이 사용되는 것을 느꼈다. 그래서 리사이클러뷰에 대해 정리하는 시간을 가져보고자 한다.
RecyclerView는 Adapter를 활용하는 컴포넌트이다. Adapter Pattern에 대해 잠깐 알아보자면 디자인 패턴의 일종으로 한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환하는 패턴이다. 이 패턴을 사용하면 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동할 수 있다. 즉, 어댑터 패턴은 호환성이 없는 두 개의 인터페이스 사이에서 중간 역할을 수행하여 서로 호환성을 유지하면서 작업을 처리할 수 있게 한다.
즉 AdapterPatten을 이용함으로써 클라이언트와 구현된 인터페이스를 분리시킬 수 있으며, 향후 인터페이스가 바뀌더라도 그 변경 내역은 어댑터에 캡슐화 되므로 클라이언트는 바뀔 필요가 없어진다. 그러므로 AdapterPattern은 한 클래스의 인터페이스를 사용자가 사용할 수 있는 인터페이스로 커스텀을 해야할 때 주로 사용한다. 이러한 Adapter Pattern을 활용하는 대표적인 컴포넌트로는 ListView와 RecyclerView가 있다. ListView와 RecyclerView는 둘다 안드로이드에서 목록을 표시하는 역할을 한다. 그러나 ListView는 기존 RecyclerView보다 먼저 사용된 리스트를 보여주는 뷰이나, 여러 문제점으로 인해 현재는 RecyclerView로 대체되는 추세이다.
그렇다면 ListView의 어떤 문제점으로 RecyclerVIew로 대체되는 추세이고 둘의 차이점은 무엇일까?
가장 먼저, 리사이클러뷰는 ViewHolder 패턴을 사용하는 반면, 리스트뷰는 사용하지 않는다는 점이 있다. 리스트뷰의 경우 목록에 있는 각 Item마다 이를 구성하는 View들을 findViewByID를 통해 연결해주어야 하는데, 이는 상당히 무거운 작업이기 때문에 Item을 생성할 때마다 이런 작업을 하게 되면, 성능 저하가 일어나게 된다.
이러한 문제를 리사이클러뷰에서는 ViewHolder패턴을 구현하여 해결한다.
(ViewHolder 패턴은 각 View를 보관하는 객체이며, layout태그 필드 안에 각 View를 저장하기 때문에 반복적인 조회가 필요 없이 즉시 액세스가 가능하다. 또한 Item 생성 시 기존에 ViewBinding 된 View객체를 재활용한다. 즉, 만들어진 View를 가져다가 Layout에 데이터만 채워주는 것이다.)
물론 리스트뷰에서도 ViewHolder 구현하는 것이 가능하다. 다만 리사이클러뷰는 ViewHolder구현이 강제라는 점이 차이점이다.
이외에도 리스트뷰는 상하 스크롤만 지원하는 반면 리사이클러뷰는 세로뿐만 아니라 가로, 지그재그등의 방향도 지원한다는 점, 리사이클러뷰는 Item들을 추가, 제거할 때 애니메이션을 처리하는 클래스가 존재하는 반면 리스트뷰는 없다는 점 등이 있다.
그렇다면 RecyclerView를 사용하는 경우에 대해 좀 더 자세히 살펴보자.
리사이클러뷰는 반복적인 형태의 뷰를 보여줘야 하는데 일부 데이터만 다르게 들어가야하는 경우 주로 사용한다. 즉 반복되는 형태의 뷰(아이템 하나)들을 보여지는 갯수 + alpha개를 미리 만들어놓고 이 뷰들을 계속해서 재활용(Recycle)하는 패턴으로 반복되는 형태의 뷰를 보여주어 리스트를 보여줄 수 있다.
다음으로 RecyclerView구현 방법을 알아보자.
RecyclerView를 만들 때는 다음 내용을 고려해야 한다.
- 어떠한 형태로 들어갈 것인지
-> 반복되는 Item의 Layout을 XML을 통해 그린다.
- 어떠한 데이터가 들어갈 것인지
-> 리스트에 들어가야하는 데이터의 형태(데이터 클래스)를 결정한다.
- 각 하나의 아이템 뷰의 어떤 부분에 어떤 데이터가 들어가야하는지
-> 재활용되는 뷰의 특정 부분에 들어온 데이터를 매칭할 수 있는 역할을 담당하는 클래스(ViewHolder)를 만든다.
- 그래서 이 데이터 배열을 뷰의 리스트(배열) 어떻게 끼워맞출 것인지
-> 데이터 리스트를 뷰홀더 리스트로 바꿔주는 Adapter를 만든다.
1. 반복되는 Item 그리기 (item_github_repo.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">
<ImageView
android:layout_width="0dp"
android:layout_height="100dp"
android:src="@drawable/dog"
android:id="@+id/iv_item_github_repo"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_item_github_repo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="레포지토리 명"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/tv_item_github_author"
android:text="레포지토리 주인"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_item_github_repo"/>
</androidx.constraintlayout.widget.ConstraintLayout>
2. 실제 배치될 뷰에 RecyclerView 배치
<?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="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fcv_home">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="vertical"/>
</androidx.constraintlayout.widget.ConstraintLayout>
이때 RecyclerView에 아이템을 배치할 수 있는 역할을 담당하는 클래스는 LayoutManager가 있다.
LinearLayoutManager - 선형으로 연속 배치
GridLayoutManager - 바둑판식 연속 배치
StaggeredGridLayoutManager - 가로 또는 세로 축에 대해 높이가 다른 그리드로 아이템을 연속 배치
FlexBoxLayoutManager - StaggeredGridLayoutManager보다 유연 ( https://github.com/google/flexbox-layout )
3. 들어갈 데이터 형태 결정 (Data Class)
반복되는 아이템에 들어갈 데이터를 Data Class로 만든다.
data class Repo(
@DrawableRes val image: Int,
val name: String,
val author:String
)
Data Class는 데이터를 보관하고 전달하기 위한 클래스를 간단하게 작성할 때 유용 클래스이다.
@DrawableRes를 이용하면 만약 이후 이 image에 리소스 값이 아닌 단순 Int값이 들어가면 컴파일 타임에 에러를 발생시킨다.
@Parcelable를 이용하면 직렬화할 수 있다.
직렬화란?
1. 메모리 내에 존재하는 정보를 보다 쉽게 전송 및 전달하기 위해 byte 코드 형태로 나열하는 것. 여기서 메모리 내에 존재하는 정보는 객체를 말함.
2. JVM의 메모리에 상주 되어있는 객체 데이터를 바이트 형태로 나타내는 기술
4. 데이터들을 View에 들어가게 하기 (ViewHolder)
이전에는 리스트들에 아이템이 있으면 아이템의 뷰에 직접 접근해서(findViewById) 하나하나 뷰의 속성을 설정해주었다면 이제는 ViewHolder 클래스에서 해당 View를 들고 그 View에 원하는 데이터를 넣어주기만 하면 된다.
ViewHolder는 클래스 내에 View를 저장하는 변수를 만들어 그 안에 데이터를 직접 연결시킬 수 있는 클래스, 디자인패턴 이다. 즉 리사이클러 뷰의 각 아이템에 데이터를 어떤 방식으로 넣을 것인지를 의미한다.
그렇다면 ViewHolder에는 어떤 것들이 필요할까?
1. 뷰를 참조하는 변수 (binding으로 가능)
2. 뷰의 멤버에 데이터를 연결시키는 기능 (함수 내에 binding.textview.text == data.~~)
class MyViewHolder(private val binding: ItemGithubRepoBinding) :
RecyclerView.ViewHolder(binding.root){
fun onBind(item: Repo) {
binding.tvItemGithubRepo.text = item.name
binding.tvItemGithubAuthor.text = item.author
}
}
5. 제작한 Layout을 실제 View에 연결시키기
위 과정을 통해 데이터만 존재하면 데이터를 뷰에 연결시킬 수 있는 ViewHolder를 만들어보았다. 다음으로 데이터를 이 View(ViewHolder에 들어갈 수 있는 것)로 변환 시킬 수 있는 Adapter를 만들어보자.
Adapter에서 구현해야할 함수는 다음과 같다.
onCreateViewHolder()
: ViewHolder에 들어갈 View를 만들어주는 함수 (즉 전체 리사이클러뷰에 관한 내용)
onBindViewHolder()
: 각각의 ViewHolder에 데이터를 매칭하는 함수 (즉 리사이클러뷰의 각각의 아이템에 관한 내)
getItemCount()
: 데이터 리스트의 아이템 갯수는 몇 개인지
class MyAdapter(context: Context) : RecyclerView.Adapter<MyViewHolder>() {
private val inflater by lazy { LayoutInflater.from(context) }
val itemlist : List<Repo> = listOf(Repo("name1","author1"),Repo("name2","author2"))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding : ItemGithubRepoBinding = ItemGithubRepoBinding.inflate(
inflater,
parent, false
)
return MyViewHolder(binding)
}
override fun getItemCount(): Int {
return itemlist.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.onBind(itemlist[position])
}
}
6. 가짜 데이터 준비하기 (서버 통신할 데이터가 있으면 pass)
추가적으로 리사이클러뷰를 업데이트는 하는 방법들에 대해 알아보자.
1. 전체 업데이트
notifyDataSetChanged()
: 어느 상황에서 사용 가능한, 쉬운 메소드로, 다시 새로 리사이클러뷰를 그려야 함을 보여준다.
주로 리스트의 크기와 아이템이 둘 다 변경될 때 사용되며, 그렇지 않다면 아래의 특정 상황에 맞는 메소드를 사용하자. 퍼포먼스 문제를 야기할 수 있다.
2. 아이템 변경
notifyItemChanged(position : Int)
: 특정 포지션의 아이템만 변경시켜야 할 경우, 이 메서드를 사용하면 된다.
position은 변경될 아이템의 위치이다. 0부터 시작한다.
notifyItemRangeChange(positionStart : Int, itemCount : Int)
: 특정 포지션부터 x개의 아이템을 변경시키는, 즉 리스트의 일부분을 수정하려면 이 메소드를 사용한다
positionStart가 가리키는 특정 아이템부터, itemCount의 수만큼의 개수를 수정한다.
즉, 위의 notifyItemChanged는 itemCount가 1로 되어있는 notifyItemRangeChanged와 같다.
3. 아이템 추가
notifyItemInserted(position : Int)
: position의 위치에 특정 아이템을 추가했다고 알린다.
position이 0이라면, 맨 위에 아이템을 추가한다.
notifyItemRangeInserted(positionStart : Int, itemCount : Int)
: positionStart에 itemCount개수만큼의 아이템을 추가했다고 알린다.
4. 아이템 삭제
notifyItemRemoved(position : Int)
: position에 위치한 아이템이 삭제되었다고 알림
notifyItemRangeRemoved(positionStart : Int, itemCount : Int)
: position에 위치한 아이템부터 itemCount개수만큼의 아이템이 삭제되었다고 알림
5. 아이템 이동
notifyItemMoved(fromPosition : Int, toPosition : Int)
: 아이템이 fromPosition에서 toPosition으로 이동했음을 알림
'Android' 카테고리의 다른 글
[Android] DataBinding (0) | 2023.08.30 |
---|---|
[Android] Multi-ViewType RecyclerView (1) | 2023.08.09 |
[Android] Fragment 생명주기 (1) | 2023.04.20 |
[Android] Activity 생명주기와 상태 (0) | 2023.04.07 |
[Android] LinearLayout (0) | 2022.12.02 |