RecyclerView는 안드로이드에서 사용할 때, 개발자가 뷰의 모양을 정의하고 대량의 데이터 목록을 화면에 출력해 동적으로 표현한다.
RecyclerView는 ListView와는 다르게 재사용성이 높은 위젯이다.
RecyclerView를 구현하기 위해 사용한 클래스는 다음과 같다.
<?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=".ListMainAcitvity">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:fontFamily="@font/kotra_hope"
android:text="List - Life"
android:textColor="#00ABF6"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/imageView"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView" />
<ImageView
android:id="@+id/imageView"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/img_list" />
<View
android:id="@+id/view"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:background="#403838"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list_life"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view"
tools:listitem="@layout/list_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
메인 레이아웃은 ConstrainLayout으로 하고 내부에 RecyclerView를 넣어주었다.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
이 코드는 리사이클러뷰의 위젯배치를 어떻게 할 것이냐를 나타낸다.
이게 없으면 실행 시 리스트가 표현되지 않는다. 적어도 난 그랬다.
tools:listitem="@layout/list_item"
이 코드는 각각의 listItem을 잘 표현했는지 가시적으로 보여주기 위해서 생긴 코드이다.
그리고 RecyclerView의 id는 반드시 부여하도록하자. 왜냐하면 어댑터를 등록할 때, id가 없으면 리사이클러뷰에 어댑터를 생성할 수 없다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/kotra_hope"
android:textSize="24sp"
android:textColor="@color/black"
android:text="내용란"/>
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/kotra_hope"
android:textSize="16sp"
android:textColor="#4E4D4D"
android:text="2022-08-10 03:03"/>
</LinearLayout>
<ImageView
android:id="@+id/btn_remove"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/img_remove"/>
</LinearLayout>
위와 같이 리스트 하나의 디자인을 구현해주어야한다. 그러면 그 하나하나의 디자인이 리사이클러뷰에 계속해서 추가되어지게 된다.
class ListInfo {
var listContent: String = "" // 메모 내용
var listDate: String = "" // 메모 일자
}
위와 같이 리스트에 들어갈 정보를 넣어줄 클래스 하나를 생성해야한다.
물론, 위 뿐만아니라 listLike, listNumber 등 어떤 정보를 넣었는지에 따라 model클래스 구현은 달라질 것이다.
Adapter는 데이터 테이블을 목록 형태로 보여주기 위해 사용되는 것으로 데이터를 다양한 형식의 리스트를 보여주기 위해서 데이터와 리스트뷰 사이에 존재하는 것이다.
-> 데이터와 리스트 뷰 사이의 통신을 위한 다리 역할을 함.
-> 데이터의 원본을 받아 관리하고, 어댑터뷰가 출력할 수 있는 형태로 데이터를 제공하는 중간 객체 역할을 함.
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.android.listapp.databinding.ListItemBinding
import com.android.listapp.model.ListInfo
// ViewHolder는 각 ItemList를 저장하는 객체이다.
class TodoAdapter : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
private var lstTodo : ArrayList<ListInfo> = ArrayList()
init {
// init은 클래스가 생성될 때 가장 먼저 호출됨. 생성자 보다 먼저 호출.
// 샘플 데이터
val todoItem = ListInfo()
todoItem.listContent = "점심 밥 먹기"
todoItem.listDate = "2022-06-01 12:23"
lstTodo.add(todoItem)
val todoItem2 = ListInfo()
todoItem2.listContent = "아침 밥 먹기"
todoItem2.listDate = "2022-06-02 08:23"
lstTodo.add(todoItem2)
val todoItem3 = ListInfo()
todoItem3.listContent = "저녁 밥 먹기"
todoItem3.listDate = "2022-06-01 18:23"
lstTodo.add(todoItem3)
}
class TodoViewHolder(private val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listItem : ListInfo) {
// 리스트 뷰 데이터를 UI에 연동
binding.tvContent.setText(listItem.listContent)
binding.tvDate.setText(listItem.listDate)
// 리스트 삭제 버튼 클릭 연동
binding.btnRemove.setOnClickListener {
// 쓰레기통 이미지 클릭 시 이벤트 처리
}
}
}
// ViewHoler가 만들어질 때. -> 뷰 홀더가 생성됨( 각 리스트 아이템 1개씩 구성될 때마다 이 오버라이드 메소드가 호출 됨 )
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoViewHolder {
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TodoViewHolder(binding)
}
// ViewHoler가 결합될 때. -> 뷰 홀더가 바인딩( 결합 )이 이루어질 때 해줘야할 처리들을 구현. position은 index 즉 배열과 비슷함.
override fun onBindViewHolder(holder: TodoAdapter.TodoViewHolder, position: Int) {
holder.bind(lstTodo[position])
}
// list의 item의 개수를 어댑터에 알려주어야함.
override fun getItemCount(): Int {
return lstTodo.size
}
}
위 코드는 어댑터뷰를 내부클래스에 구현하는 방법이다.
하나하나 쪼개서 코드를 리뷰해보자.
class TodoAdapter : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
private var lstTodo : ArrayList<ListInfo> = ArrayList()
TodoAdapter는 RecyclerView의 어댑터 클래스를 상속받는다.
<>는 제너릭 타입으로 다양한 타입의 객체에 대하여 재사용하는 프로그래밍 기법으로 java 1.5부터 추가되었다. <>안에는 타입이 들어간다.
lstTodo : ArrayList는 결과적으로 리스트에 담길 정보를 배열로 저장한다.
init {
// init은 클래스가 생성될 때 가장 먼저 호출됨. 생성자 보다 먼저 호출.
// 샘플 데이터
val todoItem = ListInfo()
todoItem.listContent = "점심 밥 먹기"
todoItem.listDate = "2022-06-01 12:23"
lstTodo.add(todoItem)
val todoItem2 = ListInfo()
todoItem2.listContent = "아침 밥 먹기"
todoItem2.listDate = "2022-06-02 08:23"
lstTodo.add(todoItem2)
val todoItem3 = ListInfo()
todoItem3.listContent = "저녁 밥 먹기"
todoItem3.listDate = "2022-06-01 18:23"
lstTodo.add(todoItem3)
}
init은 클래스가 생성될 때 가장 먼저 호출되기 때문에 더미 데이터를 넣을 때 사용하였다.
class TodoViewHolder(private val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listItem : ListInfo) {
// 리스트 뷰 데이터를 UI에 연동
binding.tvContent.setText(listItem.listContent)
binding.tvDate.setText(listItem.listDate)
// 리스트 삭제 버튼 클릭 연동
binding.btnRemove.setOnClickListener {
// 쓰레기통 이미지 클릭 시 이벤트 처리
}
}
}
위 코드는 내부클래스에 ViewHolder를 구현한 것이다.
이번 예제를 진행하면서 ViewBinding을 사용했기 때문에 findViewById는 필요 없다.
binding 변수 안에는 ListItemBinding이 들어가 있는데 내가 생성했던 xml 이름이 list_item이어서 낙타체로 ListItem으로 변환해 ViewBinding을 사용할 수 있다.
그 안에 함수 bind라는 것을 만들어 listItem 변수 안에 ListInfo들을 담아주었다.
// ViewHoler가 만들어질 때. -> 뷰 홀더가 생성됨( 각 리스트 아이템 1개씩 구성될 때마다 이 오버라이드 메소드가 호출 됨 )
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoViewHolder {
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TodoViewHolder(binding)
}
뷰 홀더가 생성될때마다 이 메소드가 호출되고, TodoViewHolder에 리턴시켜 각 정보들을 setText할 수 있다.
// ViewHoler가 결합될 때. -> 뷰 홀더가 바인딩( 결합 )이 이루어질 때 해줘야할 처리들을 구현. position은 index 즉 배열과 비슷함.
override fun onBindViewHolder(holder: TodoAdapter.TodoViewHolder, position: Int) {
holder.bind(lstTodo[position])
}
onBindViewHolder는 스크롤을 하면서 맨 위에있던 뷰 홀더( 레이아웃 )이 맨 아래로 이동해 재사용하는 것이다. 하지만 그 데이터는 새롭게 바뀌게 된다.
새롭게 보여질 데이터의 인덱스 값은 position의 이름으로 사용 가능하다.
// list의 item의 개수를 어댑터에 알려주어야함.
override fun getItemCount(): Int {
return lstTodo.size
}
마지막은 list의 item의 개수를 어댑터에게 알려주어야 한다.
그래서 결과적으로 데이터가 담겨있는 lstTodo의 .size를 사용하여 크기를 전달해준다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.android.listapp.adapter.TodoAdapter
import com.android.listapp.databinding.ActivityListMainBinding
class ListMainAcitvity : AppCompatActivity() {
private lateinit var binding: ActivityListMainBinding
private lateinit var listAdapter: TodoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityListMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 어댑터 인스턴스 생성
listAdapter = TodoAdapter()
// 리사이클러뷰에 어댑터 생성
binding.rvListLife.adapter = listAdapter
}
}
lateinit은 초기값을 설정하지 않고, 초기화를 미루는 방법으로 선언과 동시에 초기화하지 않아도 된다.
위 코드역시 ViewBindingd을 사용했다. 결과적으로 말하면
private lateinit var binding: ActivityListMainBinding
binding = ActivityListMainBinding.inflate(layoutInflater)
setContentView(binding.root)
이 3줄의 코드가 레이아웃과 연결한 것이다.
