이 Codelab은 Android Kotlin 기초 교육 과정의 일부입니다. Codelab을 순서대로 진행한다면 이 과정을 통해 최대한의 가치를 얻을 수 있을 것입니다. 모든 과정 Codelab은 Android Kotlin 기초 Codelab 방문 페이지에 나열되어 있습니다.
소개
항목을 표시하는 목록 및 그리드를 사용하는 대부분의 앱은 사용자가 항목과 상호작용할 수 있도록 합니다. 목록에서 항목을 탭한 후 항목의 세부정보를 보는 것은 이러한 유형의 상호작용에서 매우 일반적인 사용 사례입니다. 이를 위해 세부정보 보기를 표시하여 항목을 탭하는 항목에 응답하는 클릭 리스너를 추가할 수 있습니다.
이 Codelab에서는 RecyclerView
에 상호작용을 추가하여 이전 Codelab 시리즈에서 수면 추적기 앱을 확장합니다.
기본 요건
- 활동, 프래그먼트, 뷰를 사용하여 기본 사용자 인터페이스 빌드
- 프래그먼트 간 이동 및
safeArgs
을 사용하여 프래그먼트 간 데이터 전달 - 모델 보기, 모델 팩토리, 변환,
LiveData
및 그 관찰자를 봅니다. Room
데이터베이스를 만들고 데이터 액세스 객체 (DAO)를 만들고 항목을 정의하는 방법- 데이터베이스 및 기타 장기 실행 작업에 코루틴을 사용하는 방법
Adapter
,ViewHolder
, 항목 레이아웃으로 기본RecyclerView
를 구현하는 방법RecyclerView
의 데이터 결합을 구현하는 방법- 결합 어댑터를 만들고 사용하여 데이터를 변환하는 방법
GridLayoutManager
사용 방법
학습할 내용
RecyclerView
의 항목을 클릭 가능하게 만드는 방법 항목을 클릭했을 때 세부정보 보기로 이동하려면 클릭 리스너를 구현하세요.
실습할 내용
- 이 시리즈의 이전 Codelab에서 확장된 버전의 TrackMySleepQuality 앱을 기반으로 빌드합니다.
- 클릭 리스너를 목록에 추가하고 사용자 상호작용 수신을 시작합니다. 목록 항목을 탭하면 클릭된 항목의 세부정보가 포함된 프래그먼트로의 이동이 트리거됩니다. 시작 코드는 세부정보 프래그먼트와 탐색 코드를 제공합니다.
시작 수면 추적기 앱에는 아래 그림과 같이 프래그먼트로 표시되는 두 개의 화면이 있습니다.
왼쪽에 표시된 첫 번째 화면에는 추적을 시작하고 중지하는 버튼이 있습니다. 화면에 사용자의 일부 수면 데이터가 표시됩니다. 지우기 버튼은 앱에서 사용자에 대해 수집한 모든 데이터를 완전히 삭제합니다. 오른쪽에 표시된 두 번째 화면은 수면의 질 등급을 선택하기 위한 것입니다.
이 앱은 UI 컨트롤러, 뷰 모델 및 LiveData
, Room
데이터베이스를 갖춘 간소화된 아키텍처를 사용하여 수면 데이터를 유지합니다.
이 Codelab에서는 사용자가 그리드에서 항목을 탭할 때 응답하는 기능을 추가합니다. 그러면 아래와 같은 세부정보 화면이 표시됩니다. 이 화면의 코드 (프래그먼트, 뷰 모델, 탐색)는 시작 앱과 함께 제공되며 클릭 처리 메커니즘을 구현합니다.
1단계: 시작 앱 다운로드하기
- GitHub에서 RecyclerViewClickHandler-Starter 코드를 다운로드하고 Android 스튜디오에서 프로젝트를 엽니다.
- 스타터 수면 추적기 앱을 빌드하고 실행합니다.
[선택사항] 이전 Codelab의 앱을 사용하려면 앱을 업데이트하세요.
이 Codelab을 위해 GitHub에서 제공하는 시작 앱에서 작업하려면 다음 단계로 건너뛰세요.
이전 Codelab에서 빌드한 자체 수면 추적 앱을 계속 사용하려면 아래 안내에 따라 세부정보 화면 프래그먼트의 코드를 포함하도록 기존 앱을 업데이트합니다.
- 기존 앱으로 계속 진행하더라도 GitHub에서 RecyclerViewClickHandler-Starter 코드를 가져와서 파일을 복사할 수 있습니다.
sleepdetail
패키지의 모든 파일을 복사합니다.layout
폴더에서fragment_sleep_detail.xml
파일을 복사합니다.sleep_detail_fragment
의 탐색을 추가하는navigation.xml
의 업데이트된 콘텐츠를 복사합니다.database
패키지의SleepDatabaseDao
에서 새getNightWithId()
메서드
를 추가합니다.
/**
* Selects and returns the night with given nightId.
*/
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
fun getNightWithId(key: Long): LiveData<SleepNight>
res/values/strings
에서 다음 문자열 리소스를 추가합니다.
<string name="close">Close</string>
- 앱을 정리하고 다시 빌드하여 데이터 결합을 업데이트합니다.
2단계: 수면 세부정보 화면의 코드 검사
이 Codelab에서는 클릭된 수면의 밤에 관한 세부정보를 표시하는 프래그먼트로 이동하는 클릭 핸들러를 구현합니다. 시작 코드에는 이미 SleepDetailFragment
프래그먼트와 탐색 그래프가 포함되어 있습니다. 상당히 많은 코드가 포함되어 있고 프래그먼트와 탐색은 이 Codelab의 일부가 아니라는 점입니다. 다음 코드를 숙지하세요.
- 앱에서
sleepdetail
패키지를 찾습니다. 이 패키지에는 하룻밤의 수면을 표시하는 프래그먼트의 프래그먼트, 뷰 모델, 뷰 모델 팩토리가 포함되어 있습니다. sleepdetail
패키지에서SleepDetailViewModel
코드를 열고 검사합니다. 이 뷰 모델은 생성자의SleepNight
및 DAO를 사용합니다.
클래스 본문에는 지정된 키의SleepNight
를 가져오는 코드와 닫기 버튼을 누를 때SleepTrackerFragment
로 다시 이동하는 것을 제어하는navigateToSleepTracker
변수가 있습니다.getNightWithId()
함수는LiveData<SleepNight>
를 반환하고database
의SleepDatabaseDao
에서 정의됩니다.sleepdetail
패키지에서SleepDetailFragment
코드를 열고 검사합니다. 데이터 결합 설정, 뷰 모델, 탐색 관찰자를 확인하세요.sleepdetail
패키지에서SleepDetailViewModelFactory
의 코드를 열고 검사합니다.- 레이아웃 폴더에서
fragment_sleep_detail.xml
를 검사합니다.<data>
모델에서 정의된sleepDetailViewModel
변수가 뷰 모델에서 각 뷰에 표시할 데이터를 가져옵니다.
레이아웃에는 수면 품질을 위한ImageView
, 품질 평점에TextView
, 수면 길이에TextView
를 포함하고 세부정보 프래그먼트를 닫는Button
가 포함됩니다. navigation.xml
파일을 엽니다.sleep_tracker_fragment
의 경우sleep_detail_fragment
의 새 작업을 확인합니다.
새로운 작업인action_sleep_tracker_fragment_to_sleepDetailFragment
는 수면 추적기 프래그먼트에서 세부정보 화면으로 이동하는 이동입니다.
이 작업에서는 탭한 항목의 세부정보 화면을 표시하여 사용자 탭에 응답하도록 RecyclerView
를 업데이트합니다.
클릭을 수신하고 처리하는 것은 두 부분으로 구성된 작업입니다. 먼저 클릭을 수신하여 수신하고 어떤 항목을 클릭했는지 결정해야 합니다. 그런 다음 액션에 대해 클릭에 응답해야 합니다.
그렇다면 이 앱의 클릭 리스너를 추가하기에 가장 적합한 위치는 어디인가요?
SleepTrackerFragment
는 많은 뷰를 호스팅하므로 프래그먼트 수준에서 클릭 이벤트를 수신 대기하면 클릭된 항목을 알 수 없습니다. 클릭된 항목인지 다른 UI 요소 중 하나인지도 알 수 없습니다.RecyclerView
수준에서는 사용자가 목록에서 클릭한 항목을 정확히 파악하기 어렵습니다.- 클릭된 항목 하나의 정보를 가져오는 가장 좋은 속도는 목록 항목 하나를 나타내므로
ViewHolder
객체에 있는 것입니다.
ViewHolder
는 클릭을 수신 대기하기에는 좋지만 일반적으로 클릭을 처리하기에는 적합하지 않습니다. 그렇다면 클릭을 처리하기에 가장 적합한 위치는 어디인가요?
Adapter
는 뷰에 데이터 항목을 표시하므로 어댑터에서 클릭을 처리할 수 있습니다. 하지만 어댑터의 역할은 표시할 데이터를 조정하는 것이지 앱 로직을 처리하는 것이 아닙니다.- 대개
ViewModel
에서 클릭을 처리해야 합니다.ViewModel
이 클릭에 대응하여 실행할 작업을 결정하기 위한 데이터 및 로직에 액세스할 수 있기 때문입니다.
1단계: 클릭 리스너를 만들고 항목 레이아웃에서 트리거
sleeptracker
폴더에서 SleepNightAdapter.kt를 엽니다.- 파일 끝, 최상위에 새 리스너 클래스
SleepNightListener
를 만듭니다.
class SleepNightListener() {
}
SleepNightListener
클래스 내부에onClick()
함수를 추가합니다. 목록 항목을 표시하는 뷰를 클릭하면 뷰에서 이onClick()
함수를 호출합니다. 나중에 뷰의android:onClick
속성을 이 함수로 설정합니다.
class SleepNightListener() {
fun onClick() =
}
SleepNight
유형의 함수 인수night
를onClick()
에 추가합니다. 뷰는 표시되는 항목을 알고 있으며 클릭을 처리하기 위해 해당 정보를 전달해야 합니다.
class SleepNightListener() {
fun onClick(night: SleepNight) =
}
onClick()
의 기능을 정의하려면SleepNightListener
생성자에서clickListener
콜백을 제공하고onClick()
에 할당합니다.
클릭 이름을 처리하는 람다clickListener
를 지정하면 클래스 간에 전달될 때 이를 추적하는 데 도움이 됩니다.clickListener
콜백은night.nightId
만 데이터베이스의 데이터에 액세스합니다. 완성된SleepNightListener
클래스는 아래 코드와 같이 표시됩니다.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
fun onClick(night: SleepNight) = clickListener(night.nightId)
}
- list_item_sleep_night.xml을 엽니다.
data
블록 내에서 데이터 결합을 통해SleepNightListener
클래스를 사용할 수 있도록 새 변수를 추가합니다. 다음과 같이 새<variable>
에name
의clickListener.
를 지정합니다.type
를 다음과 같이 클래스com.example.android.trackmysleepquality.sleeptracker.SleepNightListener
의 정규화된 이름으로 설정합니다. 이제 이 레이아웃에서SleepNightListener
의onClick()
함수에 액세스할 수 있습니다.
<variable
name="clickListener"
type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
- 이 목록 항목의 모든 클릭을 수신 대기하려면
android:onClick
속성을ConstraintLayout
에 추가합니다.
아래와 같이 데이터 결합 람다를 사용하여 속성을clickListener:onClick(sleep)
로 설정합니다.
android:onClick="@{() -> clickListener.onClick(sleep)}"
2단계: 뷰 홀더 및 결합 객체에 클릭 리스너 전달
- SleepNightAdapter.kt를 엽니다.
val clickListener: SleepNightListener
를 받도록SleepNightAdapter
클래스의 생성자를 수정합니다. 어댑터가ViewHolder
를 바인딩할 때 이 클릭 리스너를 제공해야 합니다.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
onBindViewHolder()
에서holder.bind()
에 대한 호출을 업데이트하여 클릭 리스너도ViewHolder
에 전달합니다. 함수 호출에 매개변수를 추가했으므로 컴파일러 오류가 발생합니다.
holder.bind(getItem(position)!!, clickListener)
clickListener
매개변수를bind()
에 추가합니다. 이렇게 하려면 아래 스크린샷과 같이 오류에 커서를 놓고 오류에서Alt+Enter
(Windows) 또는Option+Enter
(Mac)을 누릅니다.
ViewHolder
클래스 내에서bind()
함수 내에서binding
객체에 클릭 리스너를 할당합니다. 결합 객체를 업데이트해야 하므로 오류가 표시됩니다.
binding.clickListener = clickListener
- 데이터 결합을 업데이트하려면 프로젝트를 정리하고 다시 빌드하세요. 캐시도 무효화해야 할 수 있습니다. 따라서 어댑터 생성자에서 클릭 리스너를 가져와 뷰 홀더와 결합 객체까지 끝까지 전달했습니다.
3단계: 항목을 탭하면 토스트 메시지 표시
이제 클릭 캡처를 위한 코드를 가지고 있지만, 목록 항목을 탭할 때 발생하는 동작을 구현하지 않았습니다. 가장 간단한 응답은 항목을 클릭할 때 nightId
를 보여주는 토스트 메시지를 표시하는 것입니다. 이렇게 하면 목록 항목을 클릭할 때 올바른 nightId
이 캡처되어 전달됩니다.
- SleepTrackerFragment.kt를 엽니다.
onCreateView()
에서adapter
변수를 찾습니다. 이제 클릭 리스너 매개변수가 예상되므로 오류가 표시됩니다.- 람다를
SleepNightAdapter
에 전달하여 클릭 리스너를 정의합니다. 이 간단한 람다는 아래와 같이nightId
를 표시하는 토스트 메시지만 표시합니다.Toast
을(를) 가져와야 합니다. 아래에는 업데이트된 전체 정의가 나와 있습니다.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
- 앱을 실행하고 항목을 탭한 다음 올바른
nightId
에 토스트 메시지가 표시되는지 확인합니다. 항목의nightId
값이 증가하여 앱이 최근 밤을 먼저 표시하므로nightId
가 가장 낮은 항목이 목록 하단에 있습니다.
이 작업에서는 RecyclerView
의 항목을 클릭할 때 동작을 변경하여 토스트 메시지를 표시하는 대신 앱이 클릭한 밤에 관한 자세한 정보를 표시하는 세부정보 프래그먼트로 이동합니다.
1단계: 클릭 시 탐색
이 단계에서는 토스트 메시지만 표시하는 대신 SleepTrackerFragment
의 onCreateView()
에서 클릭 리스너 람다를 변경하여 nightId
를 SleepTrackerViewModel
에 전달하고 SleepDetailFragment
에 탐색을 트리거합니다.
클릭 핸들러 함수:
- SleepTrackerViewModel.kt를 엽니다.
SleepTrackerViewModel
내에서 끝부분에onSleepNightClicked()
클릭 핸들러 함수를 정의합니다.
fun onSleepNightClicked(id: Long) {
}
onSleepNightClicked()
내부에서_navigateToSleepDetail
클릭된 수면 밤의 전달된id
를 설정하여 내비게이션을 트리거합니다.
fun onSleepNightClicked(id: Long) {
_navigateToSleepDetail.value = id
}
_navigateToSleepDetail
를 구현합니다. 이전에 한 것처럼 탐색 상태의private MutableLiveData
를 정의합니다. 그리고 함께 사용할 수 있는 공개val
입니다.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
get() = _navigateToSleepDetail
- 앱이 내비게이션을 완료한 후 호출할 메서드를 정의합니다. 이름을
onSleepDetailNavigated()
로 지정하고 값을null
로 설정합니다.
fun onSleepDetailNavigated() {
_navigateToSleepDetail.value = null
}
클릭 핸들러를 호출하는 코드를 추가합니다.
- SleepTrackerFragment.kt를 열고 어댑터를 만드는 코드를 아래로 스크롤하고 토스트 메시지를 표시하도록
SleepNightListener
를 정의합니다.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
- 토스트 메시지 아래에 다음 코드를 추가하여 항목을 탭할 때
sleepTrackerViewModel
에서 클릭 핸들러onSleepNighClicked()
를 호출합니다.nightId
를 전달하여 뷰 모델이 가져올 수면 시간을 알 수 있도록 합니다. 아직onSleepNightClicked()
를 정의하지 않았기 때문에 오류가 발생합니다. 토스트 메시지는 필요에 따라 보관하거나, 댓글을 달거나, 삭제할 수 있습니다.
sleepTrackerViewModel.onSleepNightClicked(nightId)
코드를 추가하여 클릭수를 관찰합니다.
- SleepTrackerFragment.kt를 엽니다.
onCreateView()
의manager
선언 바로 위에 코드를 추가하여 새navigateToSleepDetail
LiveData
를 관찰합니다.navigateToSleepDetail
가 변경되면SleepDetailFragment
로 이동하여night
를 전달하고 나중에onSleepDetailNavigated()
를 호출합니다. 이전 Codelab에서 이전에 완료했으므로 다음 코드는 다음과 같습니다.
sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepDetailFragment(night))
sleepTrackerViewModel.onSleepDetailNavigated()
}
})
- 코드를 실행하고 항목을 클릭하면 앱이 비정상 종료됩니다.
결합 어댑터에서 null 값을 처리합니다.
- 디버그 모드에서 앱을 다시 실행합니다. 항목을 탭하고 로그를 필터링하여 오류를 표시합니다. 그러면 아래와 같은 항목이 포함된 스택 트레이스가 표시됩니다.
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item
안타깝게도 스택 트레이스는 이 오류가 발생하는 위치를 명확하게 나타내지 않습니다. 데이터 결합의 단점 중 하나는 코드를 디버그하기가 더 어렵다는 점입니다. 항목을 클릭하면 앱이 비정상 종료되며, 새 코드만 클릭을 처리합니다.
하지만 이제 이 새로운 클릭 처리 메커니즘으로 item
의 null
값과 함께 결합 어댑터를 호출할 수 있습니다. 특히 앱이 시작되면 LiveData
가 null
로 시작되므로 각 어댑터에 null 검사를 추가해야 합니다.
BindingUtils.kt
에서 각 결합 어댑터의 경우item
인수의 유형을 null 허용 여부로 변경하고 본문을item?.let{...}
로 래핑합니다. 예를 들어sleepQualityString
의 어댑터는 다음과 같습니다. 다른 어댑터도 변경합니다.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
item?.let {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
}
- 앱을 실행합니다. 항목을 탭하면 세부정보 보기가 열립니다.
Android 스튜디오 프로젝트: RecyclerViewClickHandler.
RecyclerView
의 항목이 클릭에 응답하도록 하려면 클릭 리스너를 연결하여 ViewHolder
의 항목을 나열하고 ViewModel
의 클릭을 처리합니다.
RecyclerView
의 항목이 클릭에 응답하도록 하려면 다음을 실행해야 합니다.
- 람다를 사용하여
onClick()
함수에 할당하는 리스너 클래스를 만듭니다.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
fun onClick(night: SleepNight) = clickListener(night.nightId)
}
- 뷰에 클릭 리스너를 설정합니다.
android:onClick="@{() -> clickListener.onClick(sleep)}"
- 클릭 리스너를 어댑터 생성자에 전달하고 뷰 홀더에 전달하여 결합 객체에 추가합니다.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
- 어댑터를 만드는 recycler 뷰를 표시하는 프래그먼트에서 람다를 어댑터에 전달하여 클릭 리스너를 정의합니다.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
sleepTrackerViewModel.onSleepNightClicked(nightId)
})
- 뷰 모델에서 클릭 핸들러를 구현합니다. 목록 항목을 클릭하면 일반적으로 세부정보 프래그먼트로의 이동이 트리거됩니다.
Udacity 과정:
Android 개발자 문서:
이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.
- 필요한 경우 과제를 할당합니다.
- 학생에게 과제 과제를 제출하는 방법을 알려주세요.
- 과제 과제를 채점합니다.
강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.
이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.
답변
질문 1
앱에 쇼핑 목록에 상품을 표시하는 RecyclerView
가 포함되어 있다고 가정합니다. 앱은 클릭 리스너 클래스도 정의합니다.
class ShoppingListItemListener(val clickListener: (itemId: Long) -> Unit) {
fun onClick(cartItem: CartItem) = clickListener(cartItem.itemId)
}
ShoppingListItemListener
를 데이터 결합에 사용하려면 어떻게 해야 하나요? 다음 중 하나를 선택하세요.
▢ 쇼핑 목록을 표시하는 RecyclerView
가 포함된 레이아웃 파일에서 ShoppingListItemListener
의 <data>
변수를 추가합니다.
▢ 쇼핑 목록에서 한 행의 레이아웃을 정의하는 레이아웃 파일에서 ShoppingListItemListener
를 위한 <data>
변수를 추가합니다.
▢ ShoppingListItemListener
클래스에서 데이터 결합을 사용 설정하는 함수를 추가합니다.
fun onBinding (cartItem: CartItem) {dataBindingEnable(true)}
▢ ShoppingListItemListener
클래스의 onClick()
함수 내에서 데이터 결합을 사용 설정하는 호출을 추가합니다.
fun onClick(cartItem: CartItem) = {
clickListener(cartItem.itemId)
dataBindingEnable(true)
}
질문 2
RecyclerView
의 항목이 클릭에 반응하도록 하려면 android:onClick
속성을 어디에 추가하나요? 해당하는 보기를 모두 선택하세요.
▢ RecyclerView
를 표시하는 레이아웃 파일에서 <androidx.recyclerview.widget.RecyclerView>
에 추가합니다.
▢ 행의 행에 있는 항목의 레이아웃 파일에 추가합니다. 전체 항목을 클릭 가능하게 하려면 관련 행에 있는 항목이 포함된 상위 뷰에 추가합니다.
▢ 행의 행에 있는 항목의 레이아웃 파일에 추가합니다. 항목의 단일 TextView
를 클릭 가능하도록 하려면 <TextView>
에 추가합니다.
▢ 항상 MainActivity
의 레이아웃 파일 추가
다음 강의: