[Kotlin] 안드로이드 리사이클러뷰 안에 리사이클러뷰 사용해보기, 이중 리사이클러뷰 (Feat. 뷰 바인딩, 데이터 바인딩)
리사이클러뷰는 안드로이드 JetPack의 구성요소 중 하나이다.
이미 오래전부터 안드로이드 개발에서 리스트를 구현할 때에는 리사이클러뷰를 사용한다.
그만큼 유용한 기능이 많이 있다.
리사이클러 뷰를 구현하는 방법은 아래 코드를 살펴보며 차차 알아가겠지만 크게 나뉘어 보자면,
- 레이아웃 XML에 <androidx.recyclerview.widget.RecyclerView> 선언
- 리사이클러뷰 리스트의 내용물을 담을 Holder 레이아웃 파일과 클래스 생성
- 리사이클러뷰를 Holder와 연결시킬 Adapter 클래스 생성.
- 리사이클러뷰의 레이아웃 모양 결정(layoutManager로 결정한다. 보통 GridLayoutManager, LinearLayoutManager로 많이 정한다.) 그러고나서 리사이클러뷰의 어댑터를 만들어놓은 어댑터로 선언한다.
사실 오늘 정리할 부분은 리사이클러뷰의 기본 내용은 아니고, 리사이클러뷰 안에 리사이클러뷰가 있는 것을 어떻게 구현할 지 정리할 것이다.
개발을 하다보면 큰 리스트 안에 작은 리스트가 담긴 것을 본 적이 많을 것이다.
오늘 만들어 볼 화면은 결과물부터 보고가자.
위처럼 동물의 종류 리스트 - 내부에 각각 어떤 동물들이 속하는지 나누는 UI를 구현할 필요가 있다.
이 때, 리사이클러뷰를 구현할 때 데이터 바인딩을 이용해서 구현해 보겠다.
<?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=".activity.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
메인 액티비티의 화면 구성이다.
빈 화면인데 프래그먼트를 띄울 container라는 ID를 가진 제약 레이아웃이 있다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="리사이클러뷰"
android:textColor="@color/black"
android:textSize="36sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/out_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
메인 프래그먼트의 화면 구성이다.
리사이클러뷰가 길어지면 화면의 최대 크기를 넘어갈 수 있으므로, NestedScrollView로 전체 레이아웃을 감싸줬다.
크게 어려운 점은 없다 그냥 화면 구성만 한 정도이다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.khs.doublerecyclerviewusingdatabindingexampleproject.model.RecyclerOutViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.title}"
android:textColor="@color/black"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/inner_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintTop_toBottomOf="@+id/text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
바깥 리사이클러뷰의 holder 레이아웃 파일.
데이터 바인딩을 사용해서 뷰모델의 title을 최상단에 보여주고, 해당 title에 맞는
리스트를 아래에 나타나도록 했다.
보면은 TextView 아래에 리사이클러뷰 하나가 더 있다.
이 리사이클러뷰가 바로 안쪽 리사이클러뷰가 되겠다.
여기서 바깥쪽리사이클러뷰모델은 이렇게 생겼다.
title은 말 그대로 title. 오늘 구현할 화면에서는 동물의 종류가 될 것이다.
innerList는 안쪽 리사이클러뷰에 어떤 아이템을 보여줄지에 대한 리스트이다.
이렇게 만들면 각 title마다 안쪽 리사이클러뷰에 종류에 해당하는 동물들만 보여줄 수 있을 것이다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.khs.doublerecyclerviewusingdatabindingexampleproject.model.RecyclerInViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/emoji"
android:layout_width="64dp"
android:layout_height="64dp"
android:text="@{model.emoji}"
android:textSize="48dp"
android:textAlignment="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@{model.content}"
android:textColor="@color/purple_700"
android:textSize="18sp"
app:layout_constraintStart_toEndOf="@id/emoji"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
안쪽 리사이클러 뷰의 Holder 레이아웃 파일.
왼쪽에는 이모지를 띄울 것이고, 이모지 옆에는 어떤 동물인지 보여줄 것이다.
역시 데이터바인딩을 사용해서 구현했다.
안쪽 리사이클러뷰모델은 이렇게 생겼다.
레이아웃 파일을 보면 알 수 있듯이, emoji가 이모티콘, content가 동물의 이름을 나타낸다.
package com.khs.doublerecyclerviewusingdatabindingexampleproject.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.khs.doublerecyclerviewusingdatabindingexampleproject.databinding.HolderRecyclerviewOutBinding
import com.khs.doublerecyclerviewusingdatabindingexampleproject.model.RecyclerOutViewModel
class OutRecyclerViewAdapter(val context: Context, val itemList: MutableList<RecyclerOutViewModel>): RecyclerView.Adapter<OutRecyclerViewAdapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val binding = HolderRecyclerviewOutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return Holder(binding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val item = itemList[position]
holder.bind(item)
}
override fun getItemCount(): Int {
return itemList.size
}
inner class Holder(var binding: HolderRecyclerviewOutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: RecyclerOutViewModel) {
binding.model = item
binding.innerRecyclerview.adapter = InRecyclerViewAdapter(context, item.innerList)
binding.innerRecyclerview.layoutManager = LinearLayoutManager(context)
}
}
}
바깥쪽 리사이클러 뷰 어댑터 클래스와 홀더 클래스.
나같은 경우에는 어댑터 클래스와 홀더 클래스를 분리하지 않고 홀더 클래스를 어댑터 클래스 안쪽에 선언한다.
왜냐하면 어차피 하나의 Holder 클래스는 하나에 어댑터 클래스에만 대응하기 때문이다.
어댑터를 만들 때, context와 MutableList<원하는 자료형>을 매개변수로 받는다.
먼저 홀더 클래스를 살펴보면, 매개변수로 바인딩 객체를 받고, bind 함수에서
바인딩 객체의 model에 item을 바로 넣어주는 것을 볼 수 있다.
item은 바깥쪽리사이클러뷰 모델이므로 바로 넣어줄 수 있다.
지금은 바깥쪽리사이클러뷰 모델에 속하는 데이터가 하나 뿐이라서 잘 못 느끼겠지만,
여러개일 경우에 데이터 바인딩을 사용하지 않는 것보다 코드가 훨씬 간결해진다.
또한 뷰 바인딩을 사용해서, 바로 홀더 레이아웃 파일에 존재하는 안쪽리사이클러뷰에 접근을 할 수가 있다.
안쪽 리사이클러뷰의 어댑터를 만들어놓은 안쪽 리사이클러뷰 어댑터로 선언하고, item 객체에 들어있는 innerList를 안쪽 리사이클러뷰 어댑터의 List로 넘겨준다.
나는 격자형이 아닌 한줄씩 표기할 것이므로 layoutManager는 LinearLayoutManager로 선언한다.
package com.khs.doublerecyclerviewusingdatabindingexampleproject.adapter
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.khs.doublerecyclerviewusingdatabindingexampleproject.databinding.HolderRecyclerviewInBinding
import com.khs.doublerecyclerviewusingdatabindingexampleproject.model.RecyclerInViewModel
class InRecyclerViewAdapter(context: Context, val itemList: MutableList<RecyclerInViewModel>): RecyclerView.Adapter<InRecyclerViewAdapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val binding = HolderRecyclerviewInBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return Holder(binding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val item = itemList[position]
holder.bind(item)
}
override fun getItemCount(): Int {
return itemList.size
}
inner class Holder(var binding: HolderRecyclerviewInBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: RecyclerInViewModel) {
binding.model = item
}
}
}
안쪽 리사이클러뷰 어댑터.
바깥쪽 리사이클러뷰 어댑터를 잘 이해했다면 이 어댑터를 이해하는 것은 정말 쉬울 것이다.
위 코드와 중복이 많고, 크게 어려운 점이 없으므로 넘어가겠다.
package com.khs.doublerecyclerviewusingdatabindingexampleproject.activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import com.khs.doublerecyclerviewusingdatabindingexampleproject.R
import com.khs.doublerecyclerviewusingdatabindingexampleproject.databinding.ActivityMainBinding
import com.khs.doublerecyclerviewusingdatabindingexampleproject.fragment.MainFragment
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
supportFragmentManager.beginTransaction().replace(binding.container.id, MainFragment()).commit()
}
}
메인 액티비티 소스 파일.
뷰바인딩을 사용했고, onCreate될 때 supportFragmentManager를 통해서 MainFragment를 띄우도록 했다.
package com.khs.doublerecyclerviewusingdatabindingexampleproject.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.khs.doublerecyclerviewusingdatabindingexampleproject.adapter.OutRecyclerViewAdapter
import com.khs.doublerecyclerviewusingdatabindingexampleproject.databinding.FragmentMainBinding
import com.khs.doublerecyclerviewusingdatabindingexampleproject.model.RecyclerInViewModel
import com.khs.doublerecyclerviewusingdatabindingexampleproject.model.RecyclerOutViewModel
class MainFragment: Fragment() {
lateinit var binding: FragmentMainBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMainBinding.inflate(inflater, container, false)
setUpRecyclerView()
return binding.root
}
private fun setUpRecyclerView() {
var itemList = mutableListOf(
RecyclerOutViewModel("포유류", mutableListOf(
RecyclerInViewModel("🐶", "강아지"), RecyclerInViewModel("🐱", "고양이"),
RecyclerInViewModel("🐳", "고래"), RecyclerInViewModel("🦒", "사슴")
)
),
RecyclerOutViewModel("조류", mutableListOf(
RecyclerInViewModel("🦅", "독수리"), RecyclerInViewModel("🕊️", "비둘기"),
RecyclerInViewModel("🦉", "부엉이"), RecyclerInViewModel("🐔", "닭")
)
),
RecyclerOutViewModel("어류", mutableListOf(
RecyclerInViewModel("🐟", "홍어"), RecyclerInViewModel("🐟", "광어"),
RecyclerInViewModel("🐟", "연어"), RecyclerInViewModel("🐟", "우럭")
)
),
RecyclerOutViewModel("파충류", mutableListOf(
RecyclerInViewModel("🐊", "악어"), RecyclerInViewModel("🦎", "카멜레온"),
RecyclerInViewModel("🦎", "도마뱀"), RecyclerInViewModel("🐍", "뱀")
)
),
RecyclerOutViewModel("양서류", mutableListOf(
RecyclerInViewModel("🐸", "개구리"), RecyclerInViewModel("🦎", "도룡뇽"),
RecyclerInViewModel("🐸", "두꺼비")
)
),
)
binding.outRecyclerview.adapter = OutRecyclerViewAdapter(requireContext(), itemList)
binding.outRecyclerview.layoutManager = LinearLayoutManager(requireContext())
}
}
메인 프래그먼트 소스 파일.
뷰 바인딩을 사용했고, 바깥쪽 리사이클러뷰의 어댑터와 모양(레이아웃매니저)를 결정해주는 코드가 있다.
이 때, 리스트도 만들어 주는데, 리스트 안에 리스트가 안쪽 리사이클러뷰에서 보여줄 내용들이다.
쉽게말해 [ ("종류1", ["동물1", "동물2", "동물3"]), ("종류2", ["동물4", "동물5", "동물6"]) ... ] 과 같은 느낌이다.
리스트를 만들어 준 후에 adapter와 layoutManager를 설정해준다.
완성된 화면.
이런 식으로 이중 리사이클러뷰(리사이클러뷰 안에 리사이클러뷰)를 구현할 수가 있다.
샘플 프로젝트는 위에서 확인 가능하다.