이 Codelab은 Android Kotlin 기초 교육 과정의 일부입니다. Codelab을 순서대로 진행한다면 이 과정을 통해 최대한의 가치를 얻을 수 있을 것입니다. 모든 과정 Codelab은 Android Kotlin 기초 Codelab 방문 페이지에 나열되어 있습니다.
소개
이 Codelab에서는 RecyclerView
에 표시되는 목록의 너비에 걸친 헤더를 추가하는 방법을 알아봅니다. 이전 Codelab을 통해 수면 추적기 앱을 빌드했습니다.
기본 요건
- 활동, 프래그먼트, 뷰를 사용하여 기본 사용자 인터페이스를 빌드하는 방법
- 프래그먼트 간에 이동하는 방법과
safeArgs
를 사용하여 프래그먼트 간에 데이터를 전달하는 방법 - 모델 보기, 모델 팩토리, 변환,
LiveData
및 그 관찰자를 봅니다. Room
데이터베이스를 만들고 DAO를 만들고 항목을 정의하는 방법- 데이터베이스 상호작용 및 기타 장기 실행 작업에 코루틴을 사용하는 방법
Adapter
,ViewHolder
, 항목 레이아웃으로 기본RecyclerView
를 구현하는 방법RecyclerView
의 데이터 결합을 구현하는 방법- 결합 어댑터를 만들고 사용하여 데이터를 변환하는 방법
GridLayoutManager
사용 방법RecyclerView.
에서 항목 클릭을 캡처하고 처리하는 방법
학습할 내용
RecyclerView
와 함께ViewHolder
를 두 개 이상 사용하여 다른 레이아웃의 항목을 추가하는 방법 특히 두 번째ViewHolder
를 사용하여RecyclerView
에 표시된 항목 위에 헤더를 추가하는 방법을 알아야 합니다.
실습할 내용
- 이 시리즈의 이전 Codelab을 통해 TrackMySleepQuality 앱을 빌드합니다.
RecyclerView
에 표시되는 수면 밤 위에 화면 너비에 걸쳐되는 헤더를 추가합니다.
시작하는 수면 추적기 앱에는 아래 그림과 같이 프래그먼트로 표시되는 세 개의 화면이 있습니다.
왼쪽에 표시된 첫 번째 화면에는 추적을 시작하고 중지하는 버튼이 있습니다. 화면에 사용자의 일부 수면 데이터가 표시됩니다. 지우기 버튼은 앱에서 사용자에 대해 수집한 모든 데이터를 완전히 삭제합니다. 가운데 표시된 두 번째 화면은 수면의 질 등급을 선택하기 위한 것입니다. 세 번째 화면은 사용자가 그리드의 항목을 탭할 때 열리는 세부정보 뷰입니다.
이 앱은 UI 컨트롤러, 뷰 모델 및 LiveData
, Room
데이터베이스를 갖춘 간소화된 아키텍처를 사용하여 수면 데이터를 유지합니다.
이 Codelab에서는 표시된 항목 그리드에 헤더를 추가합니다. 최종 기본 화면은 다음과 같이 표시됩니다.
이 Codelab에서는 RecyclerView
에 다양한 레이아웃을 사용하는 항목을 포함하는 일반적인 원칙을 설명합니다. 한 가지 일반적인 예는 목록 또는 그리드에 헤더를 포함하는 것입니다. 목록에는 항목 콘텐츠를 설명하는 헤더가 하나만 있을 수 있습니다. 목록에 여러 헤더를 사용하여 단일 목록에서 항목을 그룹화하고 구분할 수도 있습니다.
RecyclerView
는 데이터나 각 항목의 레이아웃 유형을 전혀 모릅니다. LayoutManager
는 항목을 화면에 정렬하지만 어댑터는 표시할 데이터를 조정하고 뷰 홀더를 RecyclerView
에 전달합니다. 따라서 코드를 추가하여 어댑터에 헤더를 만듭니다.
헤더를 추가하는 두 가지 방법
RecyclerView
에서 목록의 모든 항목은 0부터 시작되는 색인 번호에 상응합니다. 예를 들면 다음과 같습니다.
[실제 데이터] -> [어댑터 뷰]
[0: SleepNight] -> [0: SleepNight]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
목록에 헤더를 추가하는 한 가지 방법은 헤더를 표시해야 하는 색인을 확인하여 다른 ViewHolder
를 사용하도록 어댑터를 수정하는 것입니다. Adapter
는 헤더를 추적하는 역할을 합니다. 예를 들어 표 상단에 헤더를 표시하려면 색인이 생성되지 않은 항목을 배치하고 헤더에 0의 다른 ViewHolder
를 반환해야 합니다. 그러면 다른 모든 항목은 아래와 같이 헤더 오프셋으로 매핑됩니다.
[실제 데이터] -> [어댑터 뷰]
[0: 헤더]
[0: SleepNight] -> [1: SleepNight]
[1: SleepNight] -> [2: SleepNight]
[2: SleepNight] -> [3: SleepNight]
헤더를 추가하는 또 다른 방법은 데이터 그리드의 지원 데이터 세트를 수정하는 것입니다. 표시해야 하는 모든 데이터는 목록에 저장되므로 헤더를 나타내는 항목을 포함하도록 목록을 수정할 수 있습니다. 이해가 좀 더 간단하지만 객체를 설계하는 방법을 생각해봐야 하므로 여러 항목 유형을 단일 목록으로 결합할 수 있습니다. 이런 방식으로 구현된 어댑터는 어댑터에 전달된 항목을 표시합니다. 따라서 게재순위 0의 항목은 헤더이고 위치 1의 항목은 SleepNight
이며 화면의 항목에 직접 매핑됩니다.
[실제 데이터] -> [어댑터 뷰]
[0: 헤더] -> [0: 헤더]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
[3: SleepNight] -> [3: SleepNight]
각 방법마다 장단점이 있습니다. 데이터 세트를 변경해도 나머지 어댑터 코드는 많이 변경되지 않으며 데이터 목록을 조작하여 헤더 로직을 추가할 수 있습니다. 반면, 헤더의 색인을 확인하여 다른 ViewHolder
를 사용하면 헤더의 레이아웃에서 더 많은 자유를 얻게 됩니다. 또한, 어댑터는 지원 데이터를 수정하지 않고도 어댑터가 데이터에 맞게 조정되는 방식을 처리할 수 있습니다.
이 Codelab에서는 RecyclerView
를 업데이트하여 목록 시작 부분에 헤더를 표시합니다. 이 경우 앱에서는 헤더에 데이터 항목과 다른 ViewHolder
를 사용합니다. 앱은 목록의 색인을 확인하여 사용할 ViewHolder
를 결정합니다.
1단계: DataItem 클래스 만들기
항목 유형을 추상화하고 어댑터가 "items"를 처리하도록 하려면 SleepNight
또는 Header
를 나타내는 데이터 홀더 클래스를 만들면 됩니다. 그러면 데이터 세트 항목이 데이터 홀더 항목 목록이 됩니다.
GitHub에서 시작 앱을 가져오거나 이전 Codelab에서 빌드한 SleepTracker 앱을 계속 사용할 수 있습니다.
- GitHub에서 RecyclerViewHeaders-Starter 코드를 다운로드합니다. RecyclerViewHeaders-Starter 디렉터리에는 이 Codelab에 필요한 SleepTracker 앱의 시작 버전이 포함되어 있습니다. 원한다면 이전 Codelab을 통해 완성된 앱을 계속 사용할 수도 있습니다.
- SleepNightAdapter.kt를 엽니다.
SleepNightListener
클래스 아래에서 최상위 수준의 데이터 항목을 나타내는DataItem
이라는sealed
클래스를 정의합니다.sealed
클래스는 닫힌 유형을 정의합니다. 즉,DataItem
의 모든 서브클래스가 이 파일에 정의되어야 합니다. 따라서 서브클래스의 수는 컴파일러에 알려져 있습니다. 코드의 다른 부분에서 어댑터를 손상시킬 수 있는 새로운 유형의DataItem
를 정의할 수는 없습니다.
sealed class DataItem {
}
DataItem
클래스 본문 내에서 서로 다른 데이터 항목 유형을 나타내는 두 개의 클래스를 정의합니다. 첫 번째는SleepNight
의 래퍼인SleepNightItem
로,sleepNight
라는 단일 값을 사용합니다. 봉인 클래스의 일부로 만들려면DataItem
를 확장합니다.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- 두 번째 클래스는
Header
이며 헤더를 나타냅니다. 헤더에 실제 데이터가 없으므로 헤더를object
로 선언할 수 있습니다. 즉,Header
인스턴스는 하나만 존재합니다. 다시DataItem
를 확장합니다.
object Header: DataItem()
DataItem
내 클래스 수준에서id
라는abstract
Long
속성을 정의합니다. 어댑터가DiffUtil
를 사용하여 항목의 변경 여부와 방법을 결정할 때DiffItemCallback
은 각 항목의 ID를 알아야 합니다.SleepNightItem
과Header
이 추상 속성id
을 재정의해야 하기 때문에 오류가 표시됩니다.
abstract val id: Long
SleepNightItem
에서id
를 재정의하여nightId
를 반환합니다.
override val id = sleepNight.nightId
Header
에서id
을 재정의하여 매우 작은 숫자 (즉, -2의 63제곱)인Long.MIN_VALUE
를 반환합니다. 따라서 존재하는nightId
와 충돌하지 않습니다.
override val id = Long.MIN_VALUE
- 완성된 코드는 다음과 같이 표시되고 오류 없이 앱이 빌드됩니다.
sealed class DataItem {
abstract val id: Long
data class SleepNightItem(val sleepNight: SleepNight): DataItem() {
override val id = sleepNight.nightId
}
object Header: DataItem() {
override val id = Long.MIN_VALUE
}
}
2단계: 헤더의 ViewHolder 만들기
TextView
를 표시하는 header.xml이라는 새 레이아웃 리소스 파일에서 헤더의 레이아웃을 만듭니다. 흥미롭지 않은 코드가 있으므로 다음과 같습니다.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Sleep Results"
android:padding="8dp" />
"Sleep Results"
을 문자열 리소스로 추출하여 이름을header_text
로 지정합니다.
<string name="header_text">Sleep Results</string>
- SleepNightAdapter.kt의
SleepNightAdapter
내에서ViewHolder
클래스 위에 새TextViewHolder
클래스를 만듭니다. 이 클래스는 textview.xml 레이아웃을 확장하고TextViewHolder
인스턴스를 반환합니다. 이전에 완료했으므로 다음 코드는 다음과 같습니다.View
및R
를 가져와야 합니다.
class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
companion object {
fun from(parent: ViewGroup): TextViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.header, parent, false)
return TextViewHolder(view)
}
}
}
3단계: SleepNightAdapter 업데이트
다음으로 SleepNightAdapter
의 선언을 업데이트해야 합니다. 한 가지 유형의 ViewHolder
만 지원하는 대신 모든 유형의 뷰 홀더를 사용할 수 있어야 합니다.
항목 유형 정의
SleepNightAdapter.kt
의 최상위 수준import
문과SleepNightAdapter
위에서 뷰 유형에 상수를 두 개 정의합니다.RecyclerView
은 뷰 홀더를 뷰 할당 항목에 올바르게 할당할 수 있도록 각 항목의 뷰 유형을 구분해야 합니다.
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 1
SleepNightAdapter
내부에서 현재 항목의 유형에 따라 오른쪽 헤더 또는 항목 상수를 반환하도록getItemViewType()
을 재정의하는 함수를 만듭니다.
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
}
}
SleepNightAdapter 정의 업데이트
SleepNightAdapter
정의에서ListAdapter
의 첫 번째 인수를SleepNight
에서DataItem
로 업데이트합니다.SleepNightAdapter
정의에서ListAdapter
의 두 번째 일반 인수를SleepNightAdapter.ViewHolder
에서RecyclerView.ViewHolder
로 변경합니다. 필요한 업데이트에 대한 오류가 표시되고 클래스 헤더는 아래와 같습니다.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {
onCreateViewHolder() 업데이트하기
onCreateViewHolder()
의 서명을 변경하여RecyclerView.ViewHolder
를 반환합니다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
onCreateViewHolder()
메서드의 구현을 펼쳐서 각 항목 유형에 적절한 뷰 홀더를 테스트하고 반환합니다. 업데이트된 메서드는 아래 코드와 같이 표시됩니다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
else -> throw ClassCastException("Unknown viewType ${viewType}")
}
}
onBindViewHolder() 업데이트하기
onBindViewHolder()
의 매개변수 유형을ViewHolder
에서RecyclerView.ViewHolder
로 변경합니다.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- 홀더가
ViewHolder
인 경우에만 뷰 홀더에 데이터를 할당하는 조건을 추가합니다.
when (holder) {
is ViewHolder -> {...}
getItem()
에서 반환된 객체 유형을DataItem.SleepNightItem
로 변환합니다. 완성된onBindViewHolder()
함수는 다음과 같습니다.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val nightItem = getItem(position) as DataItem.SleepNightItem
holder.bind(nightItem.sleepNight, clickListener)
}
}
}
diffUtil 콜백 업데이트
SleepNight
대신 새DataItem
클래스를 사용하도록SleepNightDiffCallback
의 메서드를 변경합니다. 아래 코드와 같이 린트 경고를 표시하지 않습니다.
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
헤더 추가 및 제출
SleepNightAdapter
내부의onCreateViewHolder()
아래에서 아래와 같이 함수addHeaderAndSubmitList()
를 정의합니다. 이 함수는SleepNight
목록을 사용합니다.ListAdapter
에서 제공하는submitList()
를 사용하여 목록을 제출하는 대신 이 함수를 사용하여 헤더를 추가한 다음 목록을 제출합니다.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
addHeaderAndSubmitList()
내에서 전달된 목록이null
이면 헤더만 반환하고 그렇지 않으면 헤더를 목록 헤드에 연결한 다음 목록을 제출합니다.
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
submitList(items)
- SleepTrackerFragment.kt를 열고
submitList()
호출을addHeaderAndSubmitList()
로 변경합니다.
- 앱을 실행하고 헤더가 수면 항목 목록의 첫 번째 항목으로 표시되는지 확인합니다.
이 앱에 대해 수정해야 할 두 가지가 있습니다. 하나는 표시되고 다른 하나는 표시되지 않습니다.
- 헤더가 왼쪽 상단에 표시되고 쉽게 구분할 수 없습니다.
- 헤더가 하나뿐인 간단한 목록에는 중요하지 않지만 UI 스레드의
addHeaderAndSubmitList()
에서 조작을 나열해서는 안 됩니다. 수백 개의 항목, 여러 개의 헤더, 논리가 포함된 목록을 추가하여 어디에 삽입해야 할지 결정한다고 가정해 보겠습니다. 이 작업은 코루틴에 속합니다.
코루틴을 사용하도록 addHeaderAndSubmitList()
를 변경합니다.
SleepNightAdapter
클래스 내부의 최상위 수준에서Dispatchers.Default
를 사용하여CoroutineScope
를 정의합니다.
private val adapterScope = CoroutineScope(Dispatchers.Default)
addHeaderAndSubmitList()
의adapterScope
에서 코루틴을 실행하여 목록을 조작합니다. 그런 다음Dispatchers.Main
컨텍스트로 전환하여 아래 코드와 같이 목록을 제출합니다.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {
adapterScope.launch {
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
withContext(Dispatchers.Main) {
submitList(items)
}
}
}
- 코드가 빌드되고 실행되어야 별다른 차이가 없습니다.
현재 헤더는 그리드의 다른 항목과 너비가 같아서 가로 또는 세로로 한 스팬만 차지합니다. 전체 그리드는 한 스팬 너비의 항목 세 개를 가로로 고정하므로 헤더는 스팬을 세 번 사용해야 합니다.
헤더 너비를 수정하려면 GridLayoutManager
를 모든 열에 스팬해야 하는 시기를 지정해야 합니다. 이렇게 하려면 GridLayoutManager
에 SpanSizeLookup
를 구성하면 됩니다. 이는 GridLayoutManager
가 목록의 각 항목에 사용할 스팬을 결정하는 데 사용하는 구성 객체입니다.
- SleepTrackerFragment.kt를 엽니다.
onCreateView()
끝부분에 있는manager
를 정의하는 코드를 찾습니다.
val manager = GridLayoutManager(activity, 3)
manager
아래에 다음과 같이manager.spanSizeLookup
를 정의합니다.setSpanSizeLookup
이 람다를 사용하지 않으므로object
를 만들어야 합니다. Kotlin에서object
을 만들려면object : classname
(이 경우에는GridLayoutManager.SpanSizeLookup
)를 입력합니다.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- 생성자를 호출하는 컴파일러 오류가 발생할 수 있습니다. 그러면
Option+Enter
(Mac) 또는Alt+Enter
(Windows)로 인텐트 메뉴를 열어 생성자 호출을 적용합니다.
- 그러면 메서드 재정의가 필요하다는
object
메시지가 표시됩니다.object
에 커서를 놓고Option+Enter
(Mac) 또는Alt+Enter
(Windows)를 눌러 인텐트 메뉴를 연 다음getSpanSize()
메서드를 재정의합니다.
getSpanSize()
의 본문에서 각 위치의 오른쪽 스팬 크기를 반환합니다. 게재순위 0의 스팬 크기는 3이고 다른 위치의 스팬 크기는 1입니다. 완성된 코드는 아래 코드와 같이 표시됩니다.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = when (position) {
0 -> 3
else -> 1
}
}
- 헤더의 모양을 개선하려면 header.xml을 열고 이 코드를 레이아웃 파일 header.xml에 추가합니다.
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
- 앱을 실행합니다. 아래의 스크린샷과 같이 표시됩니다.
수고하셨습니다. 이제 작업이 끝났습니다.
Android 스튜디오 프로젝트: RecyclerViewHeaders
- 헤더는 일반적으로 목록의 너비를 확장하며 제목이나 구분자 역할을 하는 항목입니다. 목록에는 상품 콘텐츠를 설명하는 단일 헤더 또는 항목을 그룹화하고 항목을 서로 분리하는 여러 헤더가 있을 수 있습니다.
RecyclerView
는 여러 뷰 홀더를 사용하여 이기종 항목(예: 헤더 및 목록 항목)을 수용할 수 있습니다.- 헤더를 추가하는 한 가지 방법은 헤더를 표시해야 하는 색인을 확인하여 다른
ViewHolder
를 사용하도록 어댑터를 수정하는 것입니다.Adapter
는 헤더를 추적하는 역할을 합니다. - 헤더를 추가하는 또 다른 방법은 데이터 그리드의 지원 데이터 세트 (목록)를 수정하는 것입니다. 이 Codelab에서는 이 작업을 수행했습니다.
다음은 헤더를 추가하는 주요 단계입니다.
- 헤더 또는 데이터를 보유할 수 있는
DataItem
를 만들어 목록의 데이터를 추상화합니다. - 어댑터의 헤더 레이아웃이 있는 뷰 홀더를 만듭니다.
- 모든 종류의
RecyclerView.ViewHolder
를 사용하도록 어댑터 및 그 메서드를 업데이트합니다. onCreateViewHolder()
에서 데이터 항목의 올바른 뷰 홀더 유형을 반환합니다.SleepNightDiffCallback
가DataItem
클래스를 사용하도록 업데이트합니다.- 코루틴을 사용하여 데이터 세트에 헤더를 추가한 다음
submitList()
를 호출하는addHeaderAndSubmitList()
함수를 만듭니다. GridLayoutManager.SpanSizeLookup()
을 구현하여 헤더 너비가 3개인 너비만 만듭니다.
Udacity 과정:
Android 개발자 문서:
이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.
- 필요한 경우 과제를 할당합니다.
- 학생에게 과제 과제를 제출하는 방법을 알려주세요.
- 과제 과제를 채점합니다.
강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.
이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.
답변
질문 1
ViewHolder
에 관한 다음 설명 중 올바른 것은 무엇인가요?
▢ 어댑터는 여러 ViewHolder
클래스를 사용하여 헤더와 다양한 유형의 데이터를 보유할 수 있습니다.
▢ 데이터를 위한 뷰 홀더 및 헤더용 뷰 홀더가 하나만 있을 수 있음
▢ A RecyclerView
는 여러 유형의 헤더를 지원하지만 데이터가 균일해야 합니다.
▢ 헤더를 추가할 때 올바른 위치에 헤더를 삽입하기 위해 RecyclerView
의 서브클래스를 생성합니다.
질문 2
RecyclerView
와 함께 코루틴을 사용해야 하는 경우 참인 문장을 모두 선택하세요.
▢ 아니요. RecyclerView
은 UI 요소이므로 코루틴을 사용하면 안 됩니다.
▢ UI를 느리게 할 수 있는 장기 실행 작업에 코루틴을 사용합니다.
▢ 목록 조작에는 시간이 오래 걸릴 수 있으며 코루틴을 사용하여 항상 조작해야 합니다.
▢ 기본 스레드를 차단하지 않으려면 정지 함수와 함께 코루틴을 사용합니다.
질문 3
다음 중 ViewHolder
를 두 개 이상 사용할 때 할 필요가 없는 것은 무엇인가요?
▢ ViewHolder
에서 필요에 따라 확장되는 레이아웃 파일을 여러 개 제공합니다.
▢ onCreateViewHolder()
에서 데이터 항목의 올바른 뷰 홀더 유형을 반환합니다.
▢ onBindViewHolder()
에서는 뷰 홀더가 데이터 항목의 올바른 뷰 홀더 유형인 경우에만 데이터를 바인딩합니다.
▢ 어댑터 클래스 서명을 일반화하여 모든 RecyclerView.ViewHolder
를 허용합니다.
다음 강의 시작:
이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.