Android Kotlin 기초 07.1: RecyclerView 기본사항

이 Codelab은 Android Kotlin 기초 과정의 일부입니다. Codelab을 순서대로 진행하면 이 과정의 학습 효과를 극대화할 수 있습니다. 모든 과정 Codelab은 Android Kotlin 기본사항 Codelab 방문 페이지에 나열되어 있습니다.

소개

이 Codelab에서는 RecyclerView를 사용하여 항목 목록을 표시하는 방법을 알아봅니다. 이전 Codelab 시리즈의 수면 추적기 앱을 기반으로 권장 아키텍처와 함께 RecyclerView를 사용하여 데이터를 더 나은 방식으로 다양하게 표시하는 방법을 알아봅니다.

기본 요건

다음을 잘 알고 있어야 합니다.

  • 활동, 프래그먼트, 뷰를 사용하여 기본 사용자 인터페이스 (UI)를 빌드합니다.
  • 프래그먼트 간 탐색 및 safeArgs을 사용하여 프래그먼트 간 데이터 전달
  • 뷰 모델, 뷰 모델 팩토리, 변환, LiveData 및 관찰자 사용
  • Room 데이터베이스 만들기, DAO 만들기, 항목 정의
  • 데이터베이스 작업 및 기타 장기 실행 작업에 코루틴 사용

학습할 내용

  • AdapterViewHolder와 함께 RecyclerView를 사용하여 항목 목록을 표시하는 방법

실습할 내용

  • 이전 강의의 TrackMySleepQuality 앱을 변경하여 RecyclerView를 사용하여 수면의 질 데이터를 표시합니다.

이 Codelab에서는 수면의 질을 추적하는 앱의 RecyclerView 부분을 빌드합니다. 이 앱은 Room 데이터베이스를 사용하여 시간 경과에 따른 수면 데이터를 저장합니다.

시작 수면 추적기 앱에는 아래 그림과 같이 프레그먼트로 표시되는 두 개의 화면이 있습니다.

왼쪽에 표시된 첫 번째 화면에는 추적을 시작하고 중지하는 버튼이 있습니다. 이 화면에는 사용자의 모든 수면 데이터도 표시됩니다. 지우기 버튼은 앱이 사용자를 위해 수집한 모든 데이터를 완전히 삭제합니다. 오른쪽에 표시된 두 번째 화면은 수면의 질 평가를 선택하는 화면입니다.

이 앱은 UI 컨트롤러, ViewModel, LiveData이 있는 단순화된 아키텍처를 사용합니다. 앱은 Room 데이터베이스를 사용하여 수면 데이터를 지속적으로 유지합니다.

첫 화면에 표시되는 수면 밤 목록은 작동하지만 예쁘지는 않습니다. 앱은 복잡한 포맷터를 사용하여 텍스트 뷰의 텍스트 문자열과 품질의 숫자를 만듭니다. 또한 이 설계는 확장되지 않습니다. 이 Codelab에서 이러한 문제를 모두 해결하면 최종 앱의 기능은 동일하며 기본 화면은 다음과 같이 표시됩니다.

데이터 목록 또는 그리드를 표시하는 것은 Android에서 가장 일반적인 UI 작업 중 하나입니다. 목록은 간단한 것부터 매우 복잡한 것까지 다양합니다. 텍스트 뷰 목록에는 쇼핑 목록과 같은 간단한 데이터가 표시될 수 있습니다. 주석이 달린 휴가 목적지 목록과 같은 복잡한 목록은 스크롤 그리드 내에 헤더와 함께 많은 세부정보를 사용자에게 표시할 수 있습니다.

이러한 모든 사용 사례를 지원하기 위해 Android에서는 RecyclerView 위젯을 제공합니다.

RecyclerView의 가장 큰 장점은 대규모 목록에 매우 효율적이라는 점입니다.

  • 기본적으로 RecyclerView는 현재 화면에 표시된 항목을 처리하거나 그리는 작업만 합니다. 예를 들어 목록에 1,000개 요소가 있지만 10개만 표시되는 경우 RecyclerView는 화면에 10개 항목을 그리기만 합니다. 사용자가 스크롤하면 RecyclerView는 화면에 어떤 새 항목이 있어야 하는지 파악하고 그 항목을 표시만 합니다.
  • 항목이 스크롤되어 화면에서 벗어나면 항목의 뷰가 재활용됩니다. 다시 말해 항목이 화면에 스크롤되는 새로운 콘텐츠로 채워집니다. 이 RecyclerView 동작은 처리 시간을 크게 단축하고 목록이 유연하게 스크롤되도록 도와줍니다.
  • 항목이 변경되면 RecyclerView는 전체 목록을 다시 그리는 대신 해당 항목 하나만 업데이트할 수 있습니다. 복잡한 항목 목록을 표시할 때 효율성이 크게 향상됩니다.

한 뷰가 아래의 순서대로 ABC 데이터로 채워진 것을 볼 수 있습니다. 이 뷰가 화면에서 스크롤된 후에는 RecyclerView는 이 뷰를 새 데이터인 XYZ에 재사용합니다.

어댑터 패턴

전기 콘센트가 다른 국가를 여행해 본 적이 있다면 어댑터를 사용하여 기기를 콘센트에 연결하는 방법을 알고 있을 것입니다. 어댑터를 사용하면 한 유형의 플러그를 다른 유형으로 변환할 수 있으며, 이는 한 인터페이스를 다른 인터페이스로 변환하는 것과 같습니다.

소프트웨어 엔지니어링의 어댑터 패턴은 객체가 다른 API와 작동하도록 지원합니다. RecyclerView는 어댑터를 사용하여 앱이 데이터를 저장하고 처리하는 방식을 변경하지 않고 앱 데이터를 RecyclerView에서 표시할 수 있는 항목으로 변환합니다. 수면 추적기 앱의 경우 ViewModel를 변경하지 않고 Room 데이터베이스의 데이터를 RecyclerView에 표시할 수 있는 형식으로 조정하는 어댑터를 빌드합니다.

RecyclerView 구현

RecyclerView에 데이터를 표시하려면 다음 부분이 필요합니다.

  • 표시할 데이터입니다.
  • 레이아웃 파일에 정의된 RecyclerView 인스턴스로, 뷰의 컨테이너 역할을 합니다.
  • 하나의 데이터 항목의 레이아웃입니다.
    모든 목록 항목이 동일하게 표시되는 경우 모든 항목에 동일한 레이아웃을 사용할 수 있지만 필수는 아닙니다. 항목 레이아웃은 한 번에 하나의 항목 뷰를 만들어 데이터로 채울 수 있도록 프래그먼트의 레이아웃과 별도로 만들어야 합니다.
  • 레이아웃 관리자입니다.
    레이아웃 관리자는 뷰에서 UI 구성요소의 구성 (레이아웃)을 처리합니다.
  • 뷰 홀더입니다.
    뷰 홀더는 ViewHolder 클래스를 확장합니다. 여기에는 항목 레이아웃에서 하나의 항목을 표시하기 위한 뷰 정보가 포함됩니다. 뷰 홀더는 RecyclerView가 화면에서 뷰를 효율적으로 이동하기 위해 사용하는 정보도 추가합니다.
  • 어댑터
    어댑터는 데이터를 RecyclerView에 연결합니다. ViewHolder에 표시될 수 있도록 데이터를 조정합니다. RecyclerView는 어댑터를 사용하여 화면에 데이터를 표시하는 방법을 파악합니다.

이 작업에서는 레이아웃 파일에 RecyclerView를 추가하고 RecyclerView에 수면 데이터를 노출하도록 Adapter를 설정합니다.

1단계: LayoutManager를 사용하여 RecyclerView 추가

이 단계에서는 fragment_sleep_tracker.xml 파일에서 ScrollViewRecyclerView으로 바꿉니다.

  1. GitHub에서 RecyclerViewFundamentals-Starter 앱을 다운로드합니다.
  2. 앱을 빌드하고 실행합니다. 데이터가 간단한 텍스트로 표시되는 방식을 확인하세요.
  3. Android 스튜디오의 Design 탭에서 fragment_sleep_tracker.xml 레이아웃 파일을 엽니다.
  4. Component Tree 창에서 ScrollView를 삭제합니다. 이 작업은 ScrollView 내부에 있는 TextView도 삭제합니다.
  5. Palette 창에서 왼쪽의 구성요소 유형 목록을 스크롤하여 Containers를 찾은 다음 선택합니다.
  6. RecyclerViewPalette 창에서 Component Tree 창으로 드래그합니다. RecyclerViewConstraintLayout 내에 배치합니다.

  1. 종속 항목을 추가할지 묻는 대화상자가 열리면 OK를 클릭하여 Android 스튜디오가 Gradle 파일에 recyclerview 종속 항목을 추가하도록 합니다. 몇 초 정도 걸릴 수 있으며, 그러면 앱이 동기화됩니다.

  1. 모듈 build.gradle 파일을 열고 끝까지 스크롤한 후 아래 코드와 비슷한 새 종속 항목을 확인합니다.
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. fragment_sleep_tracker.xml(으)로 다시 전환합니다.
  2. 텍스트 탭에서 아래에 표시된 RecyclerView 코드를 찾습니다.
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. RecyclerViewsleep_listid를 부여합니다.
android:id="@+id/sleep_list"
  1. ConstraintLayout 내에서 화면의 나머지 부분을 차지하도록 RecyclerView를 배치합니다. 이렇게 하려면 RecyclerView의 상단을 Start 버튼으로, 하단을 Clear 버튼으로, 각 측면을 상위 요소로 제한합니다. 다음 코드를 사용하여 레이아웃 편집기 또는 XML에서 레이아웃 너비와 높이를 0dp로 설정합니다.
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/clear_button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/stop_button"
  1. RecyclerView XML에 레이아웃 관리자를 추가합니다. 모든 RecyclerView에는 목록에서 항목을 배치하는 방법을 알려주는 레이아웃 관리자가 필요합니다. Android는 기본적으로 전체 너비 행의 세로 목록에 항목을 배치하는 LinearLayoutManager를 제공합니다.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. 디자인 탭으로 전환하면 추가된 제약 조건으로 인해 RecyclerView가 사용 가능한 공간을 채우도록 확장된 것을 확인할 수 있습니다.

2단계: 목록 항목 레이아웃 및 텍스트 뷰 홀더 만들기

RecyclerView는 컨테이너일 뿐입니다. 이 단계에서는 RecyclerView 내부에 표시될 항목의 레이아웃과 인프라를 만듭니다.

작동하는 RecyclerView를 최대한 빨리 얻기 위해 처음에는 수면의 질을 숫자로만 표시하는 단순한 목록 항목을 사용합니다. 이를 위해서는 뷰 홀더 TextItemViewHolder가 필요합니다. 데이터를 위한 뷰, 즉 TextView도 필요합니다. (이후 단계에서는 뷰 홀더와 모든 수면 데이터를 배치하는 방법을 자세히 알아봅니다.)

  1. text_item_view.xml이라는 레이아웃 파일을 만듭니다. 템플릿 코드를 대체하므로 루트 요소로 무엇을 사용하든 상관없습니다.
  2. text_item_view.xml에서 제공된 코드를 모두 삭제합니다.
  3. 시작과 끝에 16dp 패딩이 있고 텍스트 크기가 24spTextView를 추가합니다. 너비는 상위 요소와 일치하고 높이는 콘텐츠를 래핑하도록 합니다. 이 뷰는 RecyclerView 내에 표시되므로 뷰를 ViewGroup 내에 배치하지 않아도 됩니다.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:textSize="24sp"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:layout_width="match_parent"       
    android:layout_height="wrap_content" />
  1. Util.kt를 엽니다. 끝까지 스크롤하여 아래에 표시된 정의를 추가합니다. 그러면 TextItemViewHolder 클래스가 생성됩니다. 마지막 닫는 중괄호 뒤에 파일 하단에 코드를 넣습니다. 이 뷰 홀더는 임시이며 나중에 대체하므로 코드는 Util.kt에 들어갑니다.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. 메시지가 표시되면 android.widget.TextViewandroidx.recyclerview.widget.RecyclerView를 가져옵니다.

3단계: SleepNightAdapter 만들기

RecyclerView 구현의 핵심 작업은 어댑터를 만드는 것입니다. 상품 뷰를 위한 간단한 뷰 홀더와 각 상품의 레이아웃이 있습니다. 이제 어댑터를 만들 수 있습니다. 어댑터는 뷰 홀더를 만들고 RecyclerView에 표시할 데이터를 채웁니다.

  1. sleeptracker 패키지에서 SleepNightAdapter이라는 새 Kotlin 클래스를 만듭니다.
  2. SleepNightAdapter 클래스가 RecyclerView.Adapter를 확장하도록 합니다. 이 클래스는 SleepNight 객체를 RecyclerView에서 사용할 수 있는 것으로 조정하므로 SleepNightAdapter라고 합니다. 어댑터는 사용할 뷰 홀더를 알아야 하므로 TextItemViewHolder를 전달합니다. 메시지가 표시되면 필요한 구성요소를 가져옵니다. 구현해야 하는 필수 메서드가 있으므로 오류가 표시됩니다.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. SleepNightAdapter의 최상위 수준에서 데이터를 보유할 listOf SleepNight 변수를 만듭니다.
var data =  listOf<SleepNight>()
  1. SleepNightAdapter에서 getItemCount()를 재정의하여 data의 수면 밤 목록의 크기를 반환합니다. RecyclerView는 어댑터에 표시할 항목이 몇 개 있는지 알아야 하며, getItemCount()를 호출하여 이를 확인합니다.
override fun getItemCount() = data.size
  1. SleepNightAdapter에서 아래와 같이 onBindViewHolder() 함수를 재정의합니다.

    onBindViewHolder() 함수는 RecyclerView에 의해 호출되어 지정된 위치에 있는 목록 항목 하나의 데이터를 표시합니다. 따라서 onBindViewHolder() 메서드는 뷰 홀더와 바인딩할 데이터의 위치라는 두 가지 인수를 사용합니다. 이 앱의 경우 보유자는 TextItemViewHolder이고 위치는 목록에서의 위치입니다.
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
  1. onBindViewHolder() 내에서 데이터의 지정된 위치에 있는 항목 하나에 대한 변수를 만듭니다.
 val item = data[position]
  1. 생성한 ViewHolder에는 textView이라는 속성이 있습니다. onBindViewHolder() 내에서 textViewtext를 수면의 질 숫자로 설정합니다. 이 코드는 숫자 목록만 표시하지만 이 간단한 예시를 통해 어댑터가 데이터가 뷰 홀더로 들어가 화면에 표시되는 방식을 확인할 수 있습니다.
holder.textView.text = item.sleepQuality.toString()
  1. SleepNightAdapter에서 RecyclerView에 항목을 나타내는 뷰 홀더가 필요할 때 호출되는 onCreateViewHolder()를 재정의하고 구현합니다.

    이 함수는 두 개의 매개변수를 사용하고 ViewHolder을 반환합니다. 뷰 홀더를 보유하는 뷰 그룹인 parent 매개변수는 항상 RecyclerView입니다. viewType 매개변수는 동일한 RecyclerView에 뷰가 여러 개 있는 경우에 사용됩니다. 예를 들어 텍스트 뷰 목록, 이미지, 동영상을 모두 동일한 RecyclerView에 배치하는 경우 onCreateViewHolder() 함수는 사용할 뷰의 유형을 알아야 합니다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
  1. onCreateViewHolder()에서 LayoutInflater의 인스턴스를 만듭니다.

    레이아웃 인플레이터는 XML 레이아웃에서 뷰를 만드는 방법을 알고 있습니다. context에는 뷰를 올바르게 확장하는 방법에 관한 정보가 포함되어 있습니다. 리사이클러 뷰의 어댑터에서는 항상 RecyclerViewparent 뷰 그룹의 컨텍스트를 전달합니다.
val layoutInflater = LayoutInflater.from(parent.context)
  1. onCreateViewHolder()에서 layoutinflater에 이를 확장하도록 요청하여 view를 만듭니다.

    뷰의 XML 레이아웃과 뷰의 parent 뷰 그룹을 전달합니다. 세 번째 부울 인수는 attachToRoot입니다. 이 인수는 false여야 합니다. 왜냐하면 적절한 때가 되면 RecyclerView가 이 항목을 뷰 계층 구조에 추가하기 때문입니다.
val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. onCreateViewHolder()에서 view로 만든 TextItemViewHolder을 반환합니다.
return TextItemViewHolder(view)
  1. 어댑터는 RecyclerView가 데이터에 관해 아무것도 모르기 때문에 data가 변경될 때 RecyclerView에 알려야 합니다. 어댑터가 제공하는 뷰 홀더만 알고 있습니다.

    표시되는 데이터가 변경된 시점을 RecyclerView에 알리려면 SleepNightAdapter 클래스 상단에 있는 data 변수에 맞춤 설정자를 추가하세요. 설정자에서 data에 새 값을 부여한 다음 notifyDataSetChanged()을 호출하여 새 데이터로 목록을 다시 그립니다.
var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

4단계: RecyclerView에 어댑터 알리기

RecyclerView는 뷰 홀더를 가져오는 데 사용할 어댑터를 알아야 합니다.

  1. SleepTrackerFragment.kt를 엽니다.
  2. onCreateview()에서 어댑터를 만듭니다. 이 코드를 ViewModel 모델 생성 후, return 문 전에 배치합니다.
val adapter = SleepNightAdapter()
  1. adapterRecyclerView에 연결합니다.
binding.sleepList.adapter = adapter
  1. 프로젝트를 정리하고 다시 빌드하여 binding 객체를 업데이트합니다.

    binding.sleepList 또는 binding.FragmentSleepTrackerBinding 관련 오류가 계속 표시되면 캐시를 무효화하고 다시 시작합니다. (File > Invalidate Caches / Restart를 선택합니다.)

    이제 앱을 실행하면 오류는 없지만 Start를 탭한 다음 Stop을 탭해도 데이터가 표시되지 않습니다.

5단계: 어댑터에 데이터 가져오기

지금까지 어댑터와 어댑터에서 RecyclerView로 데이터를 가져오는 방법을 살펴보았습니다. 이제 ViewModel에서 어댑터로 데이터를 가져와야 합니다.

  1. SleepTrackerViewModel를 엽니다.
  2. 모든 수면 밤을 저장하는 nights 변수를 찾습니다. 이 변수가 표시할 데이터입니다. nights 변수는 데이터베이스에서 getAllNights()을 호출하여 설정됩니다.
  3. 이 변수에 액세스해야 하는 관찰자를 만들 것이므로 nights에서 private을 삭제합니다. 선언은 다음과 같이 표시됩니다.
val nights = database.getAllNights()
  1. database 패키지에서 SleepDatabaseDao을 엽니다.
  2. getAllNights() 함수를 찾습니다. 이 함수는 SleepNight 값 목록을 LiveData로 반환합니다. 즉, nights 변수에는 Room에 의해 업데이트되는 LiveData가 포함되어 있으며 nights를 관찰하여 변경 시점을 알 수 있습니다.
  3. SleepTrackerFragment를 엽니다.
  4. onCreateView()에서 adapter 생성 아래에 nights 변수에 대한 관찰자를 만듭니다.

    프래그먼트의 viewLifecycleOwner를 수명 주기 소유자로 제공하면 이 관찰자가 RecyclerView가 화면에 있을 때만 활성화되도록 할 수 있습니다.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. 관찰자 내에서 null이 아닌 값 (nights)을 가져올 때마다 값을 어댑터의 data에 할당합니다. 관찰자 및 데이터 설정에 대한 완성된 코드는 다음과 같습니다.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. 코드를 빌드하고 실행합니다.

    어댑터가 작동하면 수면의 질 수치가 목록으로 표시됩니다. 왼쪽 스크린샷은 시작을 탭한 후 -1을 보여줍니다. 오른쪽의 스크린샷은 중지를 탭하고 수면의 질 평가를 선택한 후 업데이트된 수면의 질 점수를 보여줍니다.

6단계: 뷰 홀더가 재활용되는 방식 살펴보기

RecyclerView는 뷰 홀더를 재활용합니다. 즉, 뷰 홀더를 재사용합니다. 뷰가 화면에서 스크롤되면 RecyclerView는 화면에 스크롤될 뷰에 뷰를 재사용합니다.

이러한 뷰 홀더는 재활용되므로 onBindViewHolder()이 이전 항목이 뷰 홀더에 설정했을 수 있는 맞춤설정을 설정하거나 재설정해야 합니다.

예를 들어 품질 등급이 1 이하이고 수면이 좋지 않음을 나타내는 뷰 홀더에서 텍스트 색상을 빨간색으로 설정할 수 있습니다.

  1. SleepNightAdapter 클래스에서 onBindViewHolder() 끝에 다음 코드를 추가합니다.
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. 앱을 실행합니다.
  2. 수면의 질이 낮은 데이터를 추가하면 숫자가 빨간색으로 표시됩니다.
  3. 화면에 빨간색의 높은 숫자가 표시될 때까지 수면 품질에 높은 평가를 추가합니다.

    RecyclerView는 뷰 홀더를 재사용하므로 결국 고품질 평가에 빨간색 뷰 홀더 중 하나를 재사용합니다. 높은 등급이 빨간색으로 잘못 표시됩니다.

  1. 이 문제를 해결하려면 품질이 1 이하가 아닌 경우 색상을 검은색으로 설정하는 else 문을 추가하세요.

    두 조건이 모두 명시되어 있으므로 뷰 홀더는 각 항목에 올바른 텍스트 색상을 사용합니다.
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. 앱을 실행하면 숫자의 색상이 항상 올바르게 표시됩니다.

축하합니다. 이제 완전히 작동하는 기본 RecyclerView가 있습니다.

이 작업에서는 간단한 뷰 홀더를 수면 밤에 관한 더 많은 데이터를 표시할 수 있는 뷰 홀더로 바꿉니다.

Util.kt에 추가한 간단한 ViewHolderTextItemViewHolder에서 TextView를 래핑합니다.

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

그렇다면 RecyclerViewTextView를 직접 사용하지 않는 이유는 무엇일까요? 이 한 줄의 코드는 많은 기능을 제공합니다. ViewHolderRecyclerView 내 위치에 관한 항목 뷰와 메타데이터를 설명합니다. RecyclerView는 이 기능을 사용하여 목록이 스크롤될 때 뷰를 올바르게 배치하고 Adapter에서 항목이 추가되거나 삭제될 때 뷰를 애니메이션하는 등의 흥미로운 작업을 실행합니다.

RecyclerViewViewHolder에 저장된 뷰에 액세스해야 하는 경우 뷰 홀더의 itemView 속성을 사용하여 액세스할 수 있습니다. RecyclerView는 화면에 표시할 항목을 바인딩할 때, 테두리와 같은 뷰 주위에 장식을 그릴 때, 접근성을 구현할 때 itemView를 사용합니다.

1단계: 항목 레이아웃 만들기

이 단계에서는 하나의 항목에 대한 레이아웃 파일을 만듭니다. 레이아웃은 수면의 질을 나타내는 ImageView, 수면 시간을 나타내는 TextView, 수면의 질을 텍스트로 나타내는 TextView로 구성된 ConstraintLayout로 구성됩니다. 이전에 레이아웃을 만든 적이 있으므로 제공된 XML 코드를 복사하여 붙여넣습니다.

  1. 새 레이아웃 리소스 파일을 만들고 이름을 list_item_sleep_night로 지정합니다.
  2. 파일의 모든 코드를 아래 코드로 바꿉니다. 그런 다음 방금 만든 레이아웃을 숙지합니다.
<?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="wrap_content">

   <ImageView
       android:id="@+id/quality_image"
       android:layout_width="@dimen/icon_size"
       android:layout_height="60dp"
       android:layout_marginStart="16dp"
       android:layout_marginTop="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:srcCompat="@drawable/ic_sleep_5" />

   <TextView
       android:id="@+id/sleep_length"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginStart="8dp"
       android:layout_marginTop="8dp"
       android:layout_marginEnd="16dp"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/quality_image"
       app:layout_constraintTop_toTopOf="@+id/quality_image"
       tools:text="Wednesday" />

   <TextView
       android:id="@+id/quality_string"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginTop="8dp"
       app:layout_constraintEnd_toEndOf="@+id/sleep_length"
       app:layout_constraintHorizontal_bias="0.0"
       app:layout_constraintStart_toStartOf="@+id/sleep_length"
       app:layout_constraintTop_toBottomOf="@+id/sleep_length"
       tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Android 스튜디오에서 디자인 탭으로 전환합니다. 디자인 뷰에서 레이아웃은 아래 왼쪽의 스크린샷과 같이 표시됩니다. 블루프린트 뷰에서는 오른쪽 스크린샷과 같이 표시됩니다.

2단계: ViewHolder 만들기

  1. SleepNightAdapter.kt를 엽니다.
  2. SleepNightAdapter 내에 ViewHolder이라는 클래스를 만들고 RecyclerView.ViewHolder를 확장하도록 합니다.
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. ViewHolder 내에서 뷰 참조를 가져옵니다. 이 ViewHolder가 업데이트할 뷰에 대한 참조가 필요합니다. 이 ViewHolder를 바인딩할 때마다 이미지와 두 텍스트 뷰에 액세스해야 합니다. (나중에 데이터 바인딩을 사용하도록 이 코드를 변환합니다.)
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)

3단계: SleepNightAdapter에서 ViewHolder 사용

  1. SleepNightAdapter 정의에서 TextItemViewHolder 대신 방금 만든 SleepNightAdapter.ViewHolder를 사용합니다.
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

onCreateViewHolder()를 업데이트합니다.

  1. ViewHolder을 반환하도록 onCreateViewHolder()의 서명을 변경합니다.
  2. 올바른 레이아웃 리소스(list_item_sleep_night)를 사용하도록 레이아웃 인플레이터를 변경합니다.
  3. TextView로의 캐스팅 삭제
  4. TextItemViewHolder를 반환하는 대신 ViewHolder를 반환합니다.

    업데이트된 onCreateViewHolder() 함수는 다음과 같습니다.
    override fun onCreateViewHolder(
            parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = 
            LayoutInflater.from(parent.context)
        val view = layoutInflater
                .inflate(R.layout.list_item_sleep_night, 
                         parent, false)
        return ViewHolder(view)
    }

onBindViewHolder()를 업데이트합니다.

  1. holder 매개변수가 TextItemViewHolder 대신 ViewHolder이 되도록 onBindViewHolder()의 서명을 변경합니다.
  2. onBindViewHolder() 내에서 item 정의를 제외한 모든 코드를 삭제합니다.
  3. 이 뷰의 resources 참조를 보유하는 val res를 정의합니다.
val res = holder.itemView.context.resources
  1. sleepLength 텍스트 뷰의 텍스트를 재생 시간으로 설정합니다. 시작 코드와 함께 제공되는 서식 지정 함수를 호출하는 아래 코드를 복사합니다.
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
  1. convertDurationToFormatted()이 정의되어야 하므로 오류가 발생합니다. Util.kt를 열고 코드와 관련 가져오기의 주석 처리를 삭제합니다. (코드 > 줄 댓글로 댓글 달기를 선택합니다.)
  2. onBindViewHolder()로 돌아가서 convertNumericQualityToString()을 사용하여 품질을 설정합니다.
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. 이러한 함수를 직접 가져와야 할 수도 있습니다.
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. 품질에 맞는 아이콘을 설정합니다. 새 ic_sleep_active 아이콘은 시작 코드에 제공됩니다.
holder.qualityImage.setImageResource(when (item.sleepQuality) {
   0 -> R.drawable.ic_sleep_0
   1 -> R.drawable.ic_sleep_1
   2 -> R.drawable.ic_sleep_2
   3 -> R.drawable.ic_sleep_3
   4 -> R.drawable.ic_sleep_4
   5 -> R.drawable.ic_sleep_5
   else -> R.drawable.ic_sleep_active
})
  1. 다음은 ViewHolder의 모든 데이터를 설정하는 업데이트된 onBindViewHolder() 함수의 완성된 버전입니다.
   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = data[position]
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. 앱을 실행합니다. 수면 효율 아이콘과 수면 시간 및 수면 효율 텍스트가 표시된 아래 스크린샷과 같은 화면이 표시됩니다.

이제 RecyclerView가 완성되었습니다. AdapterViewHolder를 구현하는 방법을 알아보고 이를 함께 사용하여 RecyclerView Adapter로 목록을 표시했습니다.

지금까지의 코드는 어댑터와 뷰 홀더를 만드는 과정을 보여줍니다. 하지만 이 코드는 개선할 수 있습니다. 표시 코드와 뷰 홀더 관리 코드가 혼합되어 있고 onBindViewHolder()ViewHolder 업데이트 방법에 관한 세부정보를 알고 있습니다.

프로덕션 앱에는 여러 뷰 홀더, 더 복잡한 어댑터, 변경사항을 적용하는 여러 개발자가 있을 수 있습니다. 뷰 홀더와 관련된 모든 항목이 뷰 홀더에만 있도록 코드를 구성해야 합니다.

1단계: onBindViewHolder() 리팩터링

이 단계에서는 코드를 리팩터링하고 모든 뷰 홀더 기능을 ViewHolder로 이동합니다. 이 리팩터링의 목적은 사용자에게 앱이 표시되는 방식을 변경하는 것이 아니라 개발자가 코드를 더 쉽고 안전하게 작업할 수 있도록 하는 것입니다. 다행히 Android 스튜디오에는 도움이 되는 도구가 있습니다.

  1. SleepNightAdapteronBindViewHolder()에서 변수 item를 선언하는 문을 제외한 모든 항목을 선택합니다.
  2. 마우스 오른쪽 버튼으로 클릭한 다음 리팩터링 > 추출 > 함수를 선택합니다.
  3. 함수 이름을 bind로 지정하고 제안된 매개변수를 허용합니다. 확인을 클릭합니다.

    bind() 함수는 onBindViewHolder() 아래에 배치됩니다.
    private fun bind(holder: ViewHolder, item: SleepNight) {
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. bind()holder 매개변수 중 holder 단어에 커서를 놓습니다. Alt+Enter (Mac의 경우 Option+Enter)를 눌러 의도 메뉴를 엽니다. Convert parameter to receiver를 선택하여 다음 서명이 있는 확장 함수로 변환합니다.
private fun ViewHolder.bind(item: SleepNight) {...}
  1. bind() 함수를 잘라내어 ViewHolder에 붙여넣습니다.
  2. bind()를 공개로 설정
  3. 필요한 경우 어댑터로 bind()을 가져옵니다.
  4. 이제 ViewHolder에 있으므로 서명의 ViewHolder 부분을 삭제할 수 있습니다. 다음은 ViewHolder 클래스의 bind() 함수의 최종 코드입니다.
fun bind(item: SleepNight) {
   val res = itemView.context.resources
   sleepLength.text = convertDurationToFormatted(
           item.startTimeMilli, item.endTimeMilli, res)
   quality.text = convertNumericQualityToString(
           item.sleepQuality, res)
   qualityImage.setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

2단계: onCreateViewHolder 리팩터링

어댑터의 onCreateViewHolder() 메서드는 현재 ViewHolder의 레이아웃 리소스에서 뷰를 확장합니다. 하지만 인플레이션은 어댑터와는 관련이 없고 ViewHolder와 관련이 있습니다. 인플레이션은 ViewHolder에서 발생해야 합니다.

  1. onCreateViewHolder()에서 함수 본문의 코드를 모두 선택합니다.
  2. 마우스 오른쪽 버튼으로 클릭한 다음 리팩터링 > 추출 > 함수를 선택합니다.
  3. 함수 이름을 from로 지정하고 제안된 매개변수를 허용합니다. 확인을 클릭합니다.
  4. 함수 이름 from에 커서를 놓습니다. Alt+Enter (Mac의 경우 Option+Enter)를 눌러 의도 메뉴를 엽니다.
  5. 컴패니언 객체로 이동을 선택합니다. from() 함수는 ViewHolder 인스턴스에서 호출되지 않고 ViewHolder 클래스에서 호출될 수 있도록 동반 객체에 있어야 합니다.
  6. companion 객체를 ViewHolder 클래스로 이동합니다.
  7. from()를 공개로 설정
  8. onCreateViewHolder()에서 ViewHolder 클래스의 from() 호출 결과를 반환하도록 return 문을 변경합니다.

    완성된 onCreateViewHolder()from() 메서드는 아래의 코드와 같이 표시되며, 코드는 오류 없이 빌드되고 실행됩니다.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: 
Int): ViewHolder {
        return ViewHolder.from(parent)
    }
companion object {
   fun from(parent: ViewGroup): ViewHolder {
       val layoutInflater = LayoutInflater.from(parent.context)
       val view = layoutInflater
               .inflate(R.layout.list_item_sleep_night, parent, false)
       return ViewHolder(view)
   }
}
  1. 생성자가 비공개가 되도록 ViewHolder 클래스의 서명을 변경합니다. 이제 from()가 새 ViewHolder 인스턴스를 반환하는 메서드이므로 더 이상 ViewHolder의 생성자를 호출할 이유가 없습니다.
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. 앱을 실행합니다. 앱이 이전과 동일하게 빌드되고 실행되어야 합니다. 이는 리팩터링 후 원하는 결과입니다.

Android 스튜디오 프로젝트: RecyclerViewFundamentals

  • 데이터 목록 또는 그리드를 표시하는 것은 Android에서 가장 일반적인 UI 작업 중 하나입니다. RecyclerView은 매우 큰 목록을 표시할 때도 효율적으로 작동하도록 설계되었습니다.
  • RecyclerView는 현재 화면에 표시된 항목을 처리하거나 그리는 데 필요한 작업만 합니다.
  • 항목이 스크롤되어 화면에서 벗어나면 뷰가 재활용됩니다. 다시 말해 항목이 화면에 스크롤되는 새로운 콘텐츠로 채워집니다.
  • 소프트웨어 엔지니어링의 어댑터 패턴은 객체가 다른 API와 함께 작동하도록 지원합니다. RecyclerView는 어댑터를 사용하여 앱이 데이터를 저장하고 처리하는 방식을 변경하지 않고도 앱 데이터를 표시할 수 있는 항목으로 변환합니다.

RecyclerView에 데이터를 표시하려면 다음 부분이 필요합니다.

  • RecyclerView
    RecyclerView 인스턴스를 만들려면 레이아웃 파일에서 <RecyclerView> 요소를 정의하세요.
  • LayoutManager
    RecyclerViewLayoutManager를 사용하여 RecyclerView의 항목 레이아웃을 구성합니다(예: 그리드 또는 선형 목록으로 배치).

    레이아웃 파일의 <RecyclerView>에서 app:layoutManager 속성을 레이아웃 관리자(예: LinearLayoutManager 또는 GridLayoutManager)로 설정합니다.

    프로그래매틱 방식으로 RecyclerViewLayoutManager를 설정할 수도 있습니다. (이 기법은 후반부 Codelab에서 다룹니다.)
  • 각 항목의 레이아웃
    XML 레이아웃 파일에서 데이터 항목 하나의 레이아웃을 만듭니다.
  • 어댑터
    데이터와 ViewHolder에 표시되는 방식을 준비하는 어댑터를 만듭니다. 어댑터를 RecyclerView와 연결합니다.

    RecyclerView이 실행되면 어댑터를 사용하여 화면에 데이터를 표시하는 방법을 파악합니다.

    어댑터에는 다음 메서드를 구현해야 합니다.
    getItemCount(): 항목 수를 반환합니다.
    onCreateViewHolder(): 목록에 있는 항목의 ViewHolder를 반환합니다.
    onBindViewHolder(): 목록에 있는 항목의 뷰에 데이터를 적용합니다.

  • ViewHolder
    ViewHolder에는 항목 레이아웃에서 하나의 항목을 표시하기 위한 뷰 정보가 포함되어 있습니다.
  • 어댑터의 onBindViewHolder() 메서드는 데이터를 뷰에 맞게 조정합니다. 이 메서드는 항상 재정의합니다. 일반적으로 onBindViewHolder()는 항목의 레이아웃을 확장하고 레이아웃의 뷰에 데이터를 배치합니다.
  • RecyclerView는 데이터에 관해 아무것도 모르므로 Adapter는 데이터가 변경될 때 RecyclerView에 알려야 합니다. notifyDataSetChanged()를 사용하여 Adapter에 데이터가 변경되었음을 알립니다.

Udacity 과정:

Android 개발자 문서:

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.

  • 필요한 경우 과제를 할당합니다.
  • 과제 제출 방법을 학생에게 알립니다.
  • 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.

이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.

질문에 답하세요

질문 1

RecyclerView는 항목을 어떻게 표시하나요? 해당하는 항목을 모두 선택해 주세요.

▢ 항목을 목록 또는 그리드로 표시합니다.

▢ 세로 또는 가로로 스크롤합니다.

▢ 태블릿과 같은 큰 기기에서는 대각선으로 스크롤합니다.

▢ 목록 또는 그리드가 사용 사례에 충분하지 않으면 맞춤 레이아웃을 허용합니다.

질문 2

RecyclerView 사용의 이점은 무엇인가요? 해당하는 항목을 모두 선택해 주세요.

▢ 큰 목록을 효율적으로 표시합니다.

▢ 데이터를 자동으로 업데이트합니다.

▢ 항목이 업데이트되거나, 삭제되거나, 목록에 추가될 때 새로고침해야 하는 필요성을 최소화합니다.

▢ 화면에서 스크롤되는 다음 항목을 표시하기 위해 화면에서 스크롤되는 뷰를 재사용합니다.

질문 3

어댑터를 사용하는 이유는 무엇인가요? 해당하는 항목을 모두 선택해 주세요.

▢ 관심 영역이 분리되면 코드를 쉽게 변경하고 테스트할 수 있습니다.

RecyclerView는 표시되는 데이터에 구애받지 않습니다.

▢ 데이터 처리 레이어는 데이터가 표시되는 방식을 신경 쓸 필요가 없습니다.

▢ 앱이 더 빠르게 실행됩니다.

질문 4

다음 중 ViewHolder에 대해 참인 것은 무엇인가요? 해당하는 항목을 모두 선택해 주세요.

ViewHolder 레이아웃은 XML 레이아웃 파일에서 정의됩니다.

▢ 데이터 세트의 각 데이터 단위별로 하나의 ViewHolder가 있습니다.

RecyclerView에 2개 이상의 ViewHolder가 있을 수 있습니다.

Adapter는 데이터를 ViewHolder에 결합합니다.

다음 강의 시작: 7.2: RecyclerView를 사용한 DiffUtil 및 데이터 결합