이 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: Header]
[0: SleepNight] -> [1: SleepNight]
[1: SleepNight] -> [2: SleepNight]
[2: SleepNight] -> [3: SleepNight.
헤더를 추가하는 또 다른 방법은 데이터 그리드의 지원 데이터 세트를 수정하는 것입니다. 표시해야 하는 모든 데이터가 목록에 저장되므로 목록을 수정하여 헤더를 나타내는 항목을 포함할 수 있습니다. 이 방법은 이해하기가 약간 더 쉽지만, 여러 항목 유형을 단일 목록으로 결합할 수 있도록 객체를 설계하는 방법을 고려해야 합니다. 이런 방식으로 구현하면 어댑터가 전달된 항목을 표시합니다. 따라서 위치 0의 항목은 헤더이고 위치 1의 항목은 화면에 직접 매핑되는 SleepNight
입니다.
[실제 데이터] -> [어댑터 뷰]
[0: Header] -> [0: Header]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
[3: SleepNight] -> [3: SleepNight]
각 방법론에는 장단점이 있습니다. 데이터 세트를 변경해도 어댑터 코드의 나머지 부분은 크게 변경되지 않으며 데이터 목록을 조작하여 헤더 로직을 추가할 수 있습니다. 반면 헤더의 색인을 확인하여 다른 ViewHolder
를 사용하면 헤더 레이아웃을 더 자유롭게 설정할 수 있습니다. 또한 어댑터가 지원 데이터를 수정하지 않고 데이터가 뷰에 적응하는 방식을 처리할 수 있습니다.
이 Codelab에서는 목록 시작 부분에 헤더를 표시하도록 RecyclerView
을 업데이트합니다. 이 경우 앱은 헤더에 데이터 항목과 다른 ViewHolder
를 사용합니다. 앱은 목록의 색인을 확인하여 사용할 ViewHolder
를 결정합니다.
1단계: DataItem 클래스 만들기
항목 유형을 추상화하고 어댑터가 '항목'만 처리하도록 하려면 SleepNight
또는 Header
을 나타내는 데이터 홀더 클래스를 만들면 됩니다. 그러면 데이터 세트는 데이터 보유자 항목 목록이 됩니다.
GitHub에서 시작 앱을 가져오거나 이전 Codelab에서 빌드한 SleepTracker 앱을 계속 사용할 수 있습니다.
- GitHub에서 RecyclerViewHeaders-Starter 코드를 다운로드합니다. RecyclerViewHeaders-Starter 디렉터리에는 이 Codelab에 필요한 SleepTracker 앱의 시작 버전이 포함되어 있습니다. 원하는 경우 이전 Codelab에서 완료한 앱을 계속 사용할 수도 있습니다.
- SleepNightAdapter.kt를 엽니다.
SleepNightListener
클래스 아래 최상위 수준에서 데이터 항목을 나타내는DataItem
라는sealed
클래스를 정의합니다.
Asealed
클래스는 닫힌 유형을 정의합니다. 즉,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
에서nightId
를 반환하도록id
를 재정의합니다.
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() 업데이트
RecyclerView.ViewHolder
을 반환하도록onCreateViewHolder()
의 서명을 변경합니다.
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)
}
}
}
- 코드가 빌드되고 실행되며 차이가 없습니다.
현재 헤더는 그리드의 다른 항목과 너비가 동일하며 가로 및 세로로 하나의 스팬을 차지합니다. 전체 그리드에는 스팬 너비가 1인 항목이 가로로 3개 들어가므로 헤더는 가로로 스팬 3개를 사용해야 합니다.
헤더 너비를 수정하려면 모든 열에 걸쳐 데이터를 표시할 시점을 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()
에서 데이터 항목에 맞는 올바른 유형의 뷰 홀더를 반환합니다.DataItem
클래스를 사용하도록SleepNightDiffCallback
업데이트- 코루틴을 사용하여 데이터 세트에 헤더를 추가한 다음
submitList()
를 호출하는addHeaderAndSubmitList()
함수를 만듭니다. GridLayoutManager.SpanSizeLookup()
를 구현하여 헤더가 3개의 스팬 너비만 되도록 합니다.
Udacity 과정:
Android 개발자 문서:
이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.
- 필요한 경우 과제를 할당합니다.
- 과제 제출 방법을 학생에게 알립니다.
- 과제를 채점합니다.
강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.
이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.
질문에 답하세요
질문 1
ViewHolder
에 관한 다음 설명 중 참인 것은 무엇인가요?
▢ 어댑터는 여러 개의 ViewHolder
클래스를 사용하여 헤더와 다양한 유형의 데이터를 보유할 수 있습니다.
▢ 데이터를 위한 뷰 홀더와 헤더를 위한 뷰 홀더를 정확히 하나씩 가질 수 있습니다.
▢ RecyclerView
는 여러 유형의 헤더를 지원하지만 데이터가 균일해야 합니다.
▢ 헤더를 추가할 때 올바른 위치에 헤더를 삽입하기 위해 RecyclerView
를 서브클래스로 만듭니다.
질문 2
RecyclerView
와 함께 코루틴을 사용해야 하는 경우는 언제인가요? 참인 문장을 모두 선택하세요.
▢ 사용 안함 RecyclerView
은 UI 요소이므로 코루틴을 사용하면 안 됩니다.
▢ UI 속도를 저하시킬 수 있는 장기 실행 작업에 코루틴을 사용합니다.
▢ 목록 조작은 시간이 오래 걸릴 수 있으므로 항상 코루틴을 사용하여 실행해야 합니다.
▢ 정지 함수가 있는 코루틴을 사용하여 기본 스레드가 차단되지 않도록 합니다.
질문 3
두 개 이상의 ViewHolder
을 사용할 때 하지 않아도 되는 것은 무엇인가요?
▢ ViewHolder
에서 필요에 따라 확장할 여러 레이아웃 파일을 제공합니다.
▢ onCreateViewHolder()
에서 데이터 항목에 맞는 올바른 유형의 뷰 홀더를 반환합니다.
▢ onBindViewHolder()
에서 뷰 홀더가 데이터 항목에 맞는 올바른 유형의 뷰 홀더인 경우에만 데이터를 바인딩합니다.
▢ 모든 RecyclerView.ViewHolder
를 허용하도록 어댑터 클래스 서명을 일반화합니다.
다음 강의 시작:
이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.