Android Kotlin 기초 07.4: RecyclerView 항목과의 상호작용

이 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단계: 시작 앱 다운로드하기

  1. GitHub에서 RecyclerViewClickHandler-Starter 코드를 다운로드하고 Android 스튜디오에서 프로젝트를 엽니다.
  2. 스타터 수면 추적기 앱을 빌드하고 실행합니다.

[선택사항] 이전 Codelab의 앱을 사용하려면 앱을 업데이트하세요.

이 Codelab을 위해 GitHub에서 제공하는 시작 앱에서 작업하려면 다음 단계로 건너뛰세요.

이전 Codelab에서 빌드한 자체 수면 추적 앱을 계속 사용하려면 아래 안내에 따라 세부정보 화면 프래그먼트의 코드를 포함하도록 기존 앱을 업데이트합니다.

  1. 기존 앱으로 계속 진행하더라도 GitHub에서 RecyclerViewClickHandler-Starter 코드를 가져와서 파일을 복사할 수 있습니다.
  2. sleepdetail 패키지의 모든 파일을 복사합니다.
  3. layout 폴더에서 fragment_sleep_detail.xml 파일을 복사합니다.
  4. sleep_detail_fragment의 탐색을 추가하는 navigation.xml의 업데이트된 콘텐츠를 복사합니다.
  5. 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>
  1. res/values/strings에서 다음 문자열 리소스를 추가합니다.
<string name="close">Close</string>
  1. 앱을 정리하고 다시 빌드하여 데이터 결합을 업데이트합니다.

2단계: 수면 세부정보 화면의 코드 검사

이 Codelab에서는 클릭된 수면의 밤에 관한 세부정보를 표시하는 프래그먼트로 이동하는 클릭 핸들러를 구현합니다. 시작 코드에는 이미 SleepDetailFragment 프래그먼트와 탐색 그래프가 포함되어 있습니다. 상당히 많은 코드가 포함되어 있고 프래그먼트와 탐색은 이 Codelab의 일부가 아니라는 점입니다. 다음 코드를 숙지하세요.

  1. 앱에서 sleepdetail 패키지를 찾습니다. 이 패키지에는 하룻밤의 수면을 표시하는 프래그먼트의 프래그먼트, 뷰 모델, 뷰 모델 팩토리가 포함되어 있습니다.

  2. sleepdetail 패키지에서 SleepDetailViewModel 코드를 열고 검사합니다. 이 뷰 모델은 생성자의 SleepNight 및 DAO를 사용합니다.

    클래스 본문에는 지정된 키의 SleepNight를 가져오는 코드와 닫기 버튼을 누를 때 SleepTrackerFragment로 다시 이동하는 것을 제어하는 navigateToSleepTracker 변수가 있습니다.

    getNightWithId() 함수는 LiveData<SleepNight>를 반환하고 databaseSleepDatabaseDao에서 정의됩니다.

  3. sleepdetail 패키지에서 SleepDetailFragment 코드를 열고 검사합니다. 데이터 결합 설정, 뷰 모델, 탐색 관찰자를 확인하세요.

  4. sleepdetail 패키지에서 SleepDetailViewModelFactory의 코드를 열고 검사합니다.

  5. 레이아웃 폴더에서 fragment_sleep_detail.xml를 검사합니다. <data> 모델에서 정의된 sleepDetailViewModel 변수가 뷰 모델에서 각 뷰에 표시할 데이터를 가져옵니다.

    레이아웃에는 수면 품질을 위한 ImageView, 품질 평점에 TextView, 수면 길이에 TextView를 포함하고 세부정보 프래그먼트를 닫는 Button가 포함됩니다.

  6. navigation.xml 파일을 엽니다. sleep_tracker_fragment의 경우 sleep_detail_fragment의 새 작업을 확인합니다.

    새로운 작업인 action_sleep_tracker_fragment_to_sleepDetailFragment는 수면 추적기 프래그먼트에서 세부정보 화면으로 이동하는 이동입니다.

이 작업에서는 탭한 항목의 세부정보 화면을 표시하여 사용자 탭에 응답하도록 RecyclerView를 업데이트합니다.

클릭을 수신하고 처리하는 것은 두 부분으로 구성된 작업입니다. 먼저 클릭을 수신하여 수신하고 어떤 항목을 클릭했는지 결정해야 합니다. 그런 다음 액션에 대해 클릭에 응답해야 합니다.

그렇다면 이 앱의 클릭 리스너를 추가하기에 가장 적합한 위치는 어디인가요?

  • SleepTrackerFragment는 많은 뷰를 호스팅하므로 프래그먼트 수준에서 클릭 이벤트를 수신 대기하면 클릭된 항목을 알 수 없습니다. 클릭된 항목인지 다른 UI 요소 중 하나인지도 알 수 없습니다.
  • RecyclerView 수준에서는 사용자가 목록에서 클릭한 항목을 정확히 파악하기 어렵습니다.
  • 클릭된 항목 하나의 정보를 가져오는 가장 좋은 속도는 목록 항목 하나를 나타내므로 ViewHolder 객체에 있는 것입니다.

ViewHolder는 클릭을 수신 대기하기에는 좋지만 일반적으로 클릭을 처리하기에는 적합하지 않습니다. 그렇다면 클릭을 처리하기에 가장 적합한 위치는 어디인가요?

  • Adapter는 뷰에 데이터 항목을 표시하므로 어댑터에서 클릭을 처리할 수 있습니다. 하지만 어댑터의 역할은 표시할 데이터를 조정하는 것이지 앱 로직을 처리하는 것이 아닙니다.
  • 대개 ViewModel에서 클릭을 처리해야 합니다. ViewModel이 클릭에 대응하여 실행할 작업을 결정하기 위한 데이터 및 로직에 액세스할 수 있기 때문입니다.

1단계: 클릭 리스너를 만들고 항목 레이아웃에서 트리거

  1. sleeptracker 폴더에서 SleepNightAdapter.kt를 엽니다.
  2. 파일 끝, 최상위에 새 리스너 클래스 SleepNightListener를 만듭니다.
class SleepNightListener() {
    
}
  1. SleepNightListener 클래스 내부에 onClick() 함수를 추가합니다. 목록 항목을 표시하는 뷰를 클릭하면 뷰에서 이 onClick() 함수를 호출합니다. 나중에 뷰의 android:onClick 속성을 이 함수로 설정합니다.
class SleepNightListener() {
    fun onClick() = 
}
  1. SleepNight 유형의 함수 인수 nightonClick()에 추가합니다. 뷰는 표시되는 항목을 알고 있으며 클릭을 처리하기 위해 해당 정보를 전달해야 합니다.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. onClick()의 기능을 정의하려면 SleepNightListener 생성자에서 clickListener 콜백을 제공하고 onClick()에 할당합니다.

    클릭 이름을 처리하는 람다 clickListener를 지정하면 클래스 간에 전달될 때 이를 추적하는 데 도움이 됩니다. clickListener 콜백은 night.nightId만 데이터베이스의 데이터에 액세스합니다. 완성된 SleepNightListener 클래스는 아래 코드와 같이 표시됩니다.
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. list_item_sleep_night.xml을 엽니다.
  2. data 블록 내에서 데이터 결합을 통해 SleepNightListener 클래스를 사용할 수 있도록 새 변수를 추가합니다. 다음과 같이 새 <variable>nameclickListener.를 지정합니다. type를 다음과 같이 클래스 com.example.android.trackmysleepquality.sleeptracker.SleepNightListener의 정규화된 이름으로 설정합니다. 이제 이 레이아웃에서 SleepNightListeneronClick() 함수에 액세스할 수 있습니다.
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. 이 목록 항목의 모든 클릭을 수신 대기하려면 android:onClick 속성을 ConstraintLayout에 추가합니다.

    아래와 같이 데이터 결합 람다를 사용하여 속성을 clickListener:onClick(sleep)로 설정합니다.
android:onClick="@{() -> clickListener.onClick(sleep)}"

2단계: 뷰 홀더 및 결합 객체에 클릭 리스너 전달

  1. SleepNightAdapter.kt를 엽니다.
  2. val clickListener: SleepNightListener를 받도록 SleepNightAdapter 클래스의 생성자를 수정합니다. 어댑터가 ViewHolder를 바인딩할 때 이 클릭 리스너를 제공해야 합니다.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. onBindViewHolder()에서 holder.bind()에 대한 호출을 업데이트하여 클릭 리스너도 ViewHolder에 전달합니다. 함수 호출에 매개변수를 추가했으므로 컴파일러 오류가 발생합니다.
holder.bind(getItem(position)!!, clickListener)
  1. clickListener 매개변수를 bind()에 추가합니다. 이렇게 하려면 아래 스크린샷과 같이 오류에 커서를 놓고 오류에서 Alt+Enter(Windows) 또는 Option+Enter(Mac)을 누릅니다.

  1. ViewHolder 클래스 내에서 bind() 함수 내에서 binding 객체에 클릭 리스너를 할당합니다. 결합 객체를 업데이트해야 하므로 오류가 표시됩니다.
binding.clickListener = clickListener
  1. 데이터 결합을 업데이트하려면 프로젝트를 정리하고 다시 빌드하세요. 캐시도 무효화해야 할 수 있습니다. 따라서 어댑터 생성자에서 클릭 리스너를 가져와 뷰 홀더와 결합 객체까지 끝까지 전달했습니다.

3단계: 항목을 탭하면 토스트 메시지 표시

이제 클릭 캡처를 위한 코드를 가지고 있지만, 목록 항목을 탭할 때 발생하는 동작을 구현하지 않았습니다. 가장 간단한 응답은 항목을 클릭할 때 nightId를 보여주는 토스트 메시지를 표시하는 것입니다. 이렇게 하면 목록 항목을 클릭할 때 올바른 nightId이 캡처되어 전달됩니다.

  1. SleepTrackerFragment.kt를 엽니다.
  2. onCreateView()에서 adapter 변수를 찾습니다. 이제 클릭 리스너 매개변수가 예상되므로 오류가 표시됩니다.
  3. 람다를 SleepNightAdapter에 전달하여 클릭 리스너를 정의합니다. 이 간단한 람다는 아래와 같이 nightId를 표시하는 토스트 메시지만 표시합니다. Toast을(를) 가져와야 합니다. 아래에는 업데이트된 전체 정의가 나와 있습니다.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. 앱을 실행하고 항목을 탭한 다음 올바른 nightId에 토스트 메시지가 표시되는지 확인합니다. 항목의 nightId 값이 증가하여 앱이 최근 밤을 먼저 표시하므로 nightId가 가장 낮은 항목이 목록 하단에 있습니다.

이 작업에서는 RecyclerView의 항목을 클릭할 때 동작을 변경하여 토스트 메시지를 표시하는 대신 앱이 클릭한 밤에 관한 자세한 정보를 표시하는 세부정보 프래그먼트로 이동합니다.

1단계: 클릭 시 탐색

이 단계에서는 토스트 메시지만 표시하는 대신 SleepTrackerFragmentonCreateView()에서 클릭 리스너 람다를 변경하여 nightIdSleepTrackerViewModel에 전달하고 SleepDetailFragment에 탐색을 트리거합니다.

클릭 핸들러 함수:

  1. SleepTrackerViewModel.kt를 엽니다.
  2. SleepTrackerViewModel 내에서 끝부분에 onSleepNightClicked() 클릭 핸들러 함수를 정의합니다.
fun onSleepNightClicked(id: Long) {

}
  1. onSleepNightClicked() 내부에서 _navigateToSleepDetail 클릭된 수면 밤의 전달된 id 를 설정하여 내비게이션을 트리거합니다.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. _navigateToSleepDetail를 구현합니다. 이전에 한 것처럼 탐색 상태의 private MutableLiveData를 정의합니다. 그리고 함께 사용할 수 있는 공개 val입니다.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. 앱이 내비게이션을 완료한 후 호출할 메서드를 정의합니다. 이름을 onSleepDetailNavigated()로 지정하고 값을 null로 설정합니다.
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

클릭 핸들러를 호출하는 코드를 추가합니다.

  1. SleepTrackerFragment.kt를 열고 어댑터를 만드는 코드를 아래로 스크롤하고 토스트 메시지를 표시하도록 SleepNightListener를 정의합니다.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. 토스트 메시지 아래에 다음 코드를 추가하여 항목을 탭할 때 sleepTrackerViewModel에서 클릭 핸들러 onSleepNighClicked()를 호출합니다. nightId를 전달하여 뷰 모델이 가져올 수면 시간을 알 수 있도록 합니다. 아직 onSleepNightClicked()를 정의하지 않았기 때문에 오류가 발생합니다. 토스트 메시지는 필요에 따라 보관하거나, 댓글을 달거나, 삭제할 수 있습니다.
sleepTrackerViewModel.onSleepNightClicked(nightId)

코드를 추가하여 클릭수를 관찰합니다.

  1. SleepTrackerFragment.kt를 엽니다.
  2. 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()
            }
        })
  1. 코드를 실행하고 항목을 클릭하면 앱이 비정상 종료됩니다.

결합 어댑터에서 null 값을 처리합니다.

  1. 디버그 모드에서 앱을 다시 실행합니다. 항목을 탭하고 로그를 필터링하여 오류를 표시합니다. 그러면 아래와 같은 항목이 포함된 스택 트레이스가 표시됩니다.
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

안타깝게도 스택 트레이스는 이 오류가 발생하는 위치를 명확하게 나타내지 않습니다. 데이터 결합의 단점 중 하나는 코드를 디버그하기가 더 어렵다는 점입니다. 항목을 클릭하면 앱이 비정상 종료되며, 새 코드만 클릭을 처리합니다.

하지만 이제 이 새로운 클릭 처리 메커니즘으로 itemnull 값과 함께 결합 어댑터를 호출할 수 있습니다. 특히 앱이 시작되면 LiveDatanull로 시작되므로 각 어댑터에 null 검사를 추가해야 합니다.

  1. BindingUtils.kt에서 각 결합 어댑터의 경우 item 인수의 유형을 null 허용 여부로 변경하고 본문을 item?.let{...}로 래핑합니다. 예를 들어 sleepQualityString의 어댑터는 다음과 같습니다. 다른 어댑터도 변경합니다.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. 앱을 실행합니다. 항목을 탭하면 세부정보 보기가 열립니다.

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의 레이아웃 파일 추가

다음 강의: 7.5: RecyclerView의 헤더