[Android/Kotlin] Jetpack 리사이클러 뷰(RecyclerView)로 목록 화면 구현하기
RecyclerView
Jetpack(androidx 라이브러리)의 구성요소 중 하나로, 여러 가지 항목을 나열하는 목록화면을 만들 때 사용한다.
대용량 데이터를 화면에 리스트 형태로 출력할 때, 데이터셋의 각 데이터는 하나의 뷰로 표현된다. 리스트의 항목이 여러 개일 경우 화면을 넘어가는 정보는 스크롤로 내려서 확인할 수 있는데, RecyclerView에서는 스크롤을 내림으로써 화면밖으로 사라진 뷰를 제거하지 않고 새롭게 나타난 뷰로 재활용한다. (=말그대로 View를 Recycle하는 방식으로 동작함) 앱에 표시할 데이터가 총 100개이고, 한 화면에 표시할 수 있는 데이터는 10개가 한계일 경우 굳이 100개의 View를 만들지 않고 10~12개 정도의 실제 화면에 표시된 View들만을 만들어 둔 뒤 화면이 변경될 때마다(스크롤 이동) 방금 화면 밖으로 사라진 View를 새롭게 나타난 View로 활용할 수 있다.
RecyclerView 구성요소
- RecyclerView : 데이터에 해당하는 뷰가 포함된 ViewGroup. (androidx.recyclerview.widget.RecyclerView)
- ViewHolder : 목록의 개별 항목, 각 항목을 구성하는 뷰 객체를 가진다. (RecyclerView.ViewHolder)
- Adapter : 항목 구성, ViewHolder에 있는 뷰 객체에 데이터를 연결(binding)한다. (RecyclerView.Adapter)
- LayoutManager : 항목 배치 (기본 제공 LayoutManager 사용 or 직접 정의)
- ItemDecoration : 항목 디자인 요소 추가
RecyclerView 구현 단계
*필요한 작업 파일
1. gradle파일 dependencies 항목 선언 [build.gradle (Module:프로젝트명.모듈명)]
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.1.0'
// 생략
}
2. [res → values → themes] 파일을 수정해 테마를 NoActionBar로 설정 [themes.xml]
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.AndroidLab" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- 생략 -->
</style>
</resources>
3. RecyclerView 레이아웃 XML 파일 작성 [activity_recyclerview.xml]
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/recyclerView"
android:layout_height="match_parent"/>
4. RecyclerView에 표시할 항목을 디자인한 XML 파일 생성 [item_main.xml]
여기서 선언된 <TextView>레이아웃 요소가 ViewHolder에 포함되는 뷰 객체. 즉, 데이터가 바인딩 되는 대상이다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_root"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/item_data"
android:layout_weight="1"
android:textStyle="bold"
android:textSize="16dp" />
</LinearLayout>
5. RecyclerView.ViewHolder를 상속하는 ViewHolder 정의 [MainActivity.kt]
*원래 따로 코틀린 파일(Fragement)을 분리해야 하지만, 실습상 편의를 위해 MainActivity.kt 파일에 모아서 작성함
class MyViewHolder(val binding: ItemMainBinding) : RecyclerView.ViewHolder(binding.root)
6. RecyclerView.Adapter를 상속하는 Adapter 정의, Adapter 메서드를 사용해서 ViewHolder가 가지고 있는 View객체에 데이터를 연결한다. (=binding)
class MyAdapter(val datas: MutableList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>(){
// ViewHolder, 뷰 객체 초기화
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return MyViewHolder(ItemMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
// ViewHolder의 뷰 객체에 데이터 바인딩
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
Log.d("kkang", "onBindViewHolder")
val binding = (holder as MyViewHolder).binding
// 뷰에 데이터 출력
binding.itemData.text = datas[position]
// 뷰에 이벤트 등록
binding.itemRoot.setOnClickListener {
Log.d("kkang", "item root click : $position")
}
}
// 총 데이터 개수 반환
override fun getItemCount(): Int {
return datas.size
}
}
Adapter 메서드
- onCreateViewHolder() : RecyclerView가 ViewHolder를 새로 생성할 때 호출하는 메서드. ViewHolder와 그에 연결된 View를 생성하고 초기화한다.
- onBindViewHolder() : RecyclerView가 ViewHolder에 데이터를 연결할 때 호출하는 메서드. 데이터를 가져와서 그 데이터로 ViewHolder의 레이아웃을 채운다.
(ex: 이름 목록 데이터를 가져와서 ViewHolder의 TextView 객체를 채움) - getItemCount() : 총 데이터 개수 반환, 여기서 반환된 숫자만큼 onBindViewHolder() 함수가 호출되고, 항목을 추가로 표시할 수 없는 상황이면 0을 반환한다.
7. RecyclerView 화면 출력
기존에 있던 화면 출력 담당 onCreate() 메서드 내 코드를 수정한다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// RecyclerView 선언된 XML 파일을 메인화면으로 출력
val binding = ActivityRecyclerviewBinding.inflate(layoutInflater)
setContentView(binding.root)
// View 객체에 연결할 데이터 세트
val datas = mutableListOf<String>()
for(i in 1..10){
datas.add("Item $i")
}
// RecyclerView 설정
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = MyAdapter(datas)
binding.recyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
}
}
8. LayoutManager를 사용해 RecyclerView의 항목을 정렬할 수 있다. (=RecyclerView.layoutManager 속성 설정)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
- LinearLayoutManager : 항목을 1차원 목록으로 정렬
- GridLayoutManager : 항목을 2차원 그리드로 정렬, 각 행/열의 너비/높이를 자동맞춤
- StaggeredGridLayoutManager : GridLayoutManger와 유사하나 각 행/열의 너비/높이 자유
9. ItemDecoration으로 RecyclerView에 디자인을 추가할 수 있다. (=RecyclerView.addItemDecoration() 메서드 호출)
binding.recyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
메인 소스코드
[MainActivity.kt]
package com.example.androidlab
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.androidlab.databinding.ActivityRecyclerviewBinding
import com.example.androidlab.databinding.ItemMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// RecyclerView 선언된 XML 파일을 메인화면으로 출력
val binding = ActivityRecyclerviewBinding.inflate(layoutInflater)
setContentView(binding.root)
// View 객체에 연결할 데이터 세트
val datas = mutableListOf<String>()
for(i in 1..10){
datas.add("Item $i")
}
// RecyclerView 설정
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = MyAdapter(datas)
binding.recyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
}
}
// ViewHolder
class MyViewHolder(val binding: ItemMainBinding) : RecyclerView.ViewHolder(binding.root)
// Adapter
class MyAdapter(val datas: MutableList<String>) : RecyclerView.Adapter<RecyclerView.ViewHolder>(){
// ViewHolder, 뷰 객체 초기화
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return MyViewHolder(ItemMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
// ViewHolder의 뷰 객체에 데이터 바인딩
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
Log.d("kkang", "onBindViewHolder")
val binding = (holder as MyViewHolder).binding
// 뷰에 데이터 출력
binding.itemData.text = datas[position]
// 뷰에 이벤트 등록
binding.itemRoot.setOnClickListener {
Log.d("kkang", "item root click : $position")
}
}
// 총 데이터 개수 반환
override fun getItemCount(): Int {
return datas.size
}
}
참고자료
- Android Developer - RecyclerView로 동적 목록 만들기, https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=ko
- Android Developer - GridLayoutManager, https://developer.android.com/reference/androidx/recyclerview/widget/GridLayoutManager?hl=ko
- Do it! 깡쌤의 안드로이드 프로그래밍 with Kotlin