본문 바로가기

안드로이드 Android/Android Study Jams

7. Databases and RecyclerView - RecyclerView Fundamentals

RecyclerView

RecyclerView 구현하기

 

Implement RecyclerView and an Adapter

 

Step 1: LayoutManager와 RecyclerView 추가하기

github.com/google-developer-training/android-kotlin-fundamentals-starter-apps/tree/master/RecyclerViewFundamentals-Starter

 

google-developer-training/android-kotlin-fundamentals-starter-apps

android-kotlin-fundamentals-starter-apps. Contribute to google-developer-training/android-kotlin-fundamentals-starter-apps development by creating an account on GitHub.

github.com

fragment_sleep_tracker.xml

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/sleep_list"
            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/start_button"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

기존의 ScrollView를 삭제하고 RecyclerView를 추가한다.

 

build.gradle (Module)

implementation 'androidx.recyclerview:recyclerview:1.1.0'

 

Step 2: 리스트 아이템 레이아웃과 텍스트 뷰 홀더 생성하기

text_item_view.xml

<?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" />

 

Util.kt

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

이 뷰 홀더는 일시적인 것이고 나중에 바꿀 것이므로 코드를 Util.kt에 둔다.

 

Step 3: SleepNightAdapter 생성하기

SleepNightAdapter.kt

class SleepNightAdapter : RecyclerView.Adapter<TextItemViewHolder>() {
    var data = listOf<SleepNight>()
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    override fun getItemCount() = data.size

    override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
        val item = data[position]

        holder.textView.text = item.sleepQuality.toString()
    }

    // RecyclerView가 view holder를 필요로 할 때 호출 됨
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)

        val view = layoutInflater
                .inflate(R.layout.text_item_view, parent, false) as TextView

        return TextItemViewHolder(view)
    }
}

RecyclerView가 띄우고 있는 데이터가 언제 변하는 지 알려주기 위해 data 변수에 커스텀 setter를 추가한다. setter에서 data에 새 값을 주고 나서 새 데이터로 리스트를 다시 구성하기 위해 notifyDataSetChanged()를 호출한다.

Note: notifyDataSetChanged()가 호출되면 RecyclerView가 바뀐 아이템 뿐만 아니라 리스트 전체를 재구성한다. 이는 나중에 개선할 것이다.

onCreateViewHolder의 parent는 view holder를 갖고 있는 view group인데 이는 항상 RecyclerView이다. viewType은 같은 RecyclerView에 여러 개의 뷰가 있을 때 사용된다.

inflate()의 세 번째 boolean 인자는 attachToRoot이다. RecyclerView는 이 아이템을 제때 뷰에 추가하므로 이 인자는 false로 해야 한다.

 

Step 4: RecyclerView에게 Adapter에 대해 알려주기

SleepTrackerFragment.kt

...
val adapter = SleepNightAdapter()
...
binding.sleepList.adapter = adapter
...

 

Step 5: 어댑터에 데이터 얻기

SleepTrackerViewModel.kt

val nights = database.getAllNights()

nights 변수에 접근해야 하는 옵저버를 생성할 것이므로 이 변수에서 private을 제거한다.

 

SleepTrackerFragment.kt

...
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
...

라이프사이클 오너로 프래그먼트의 viewLifecycleOwner를 제공함으로써 RecyclerView가 화면에 있을 때만 이 옵저버가 활성화되도록 할 수 있다.

 

Step 6: 어떻게 뷰 홀더가 재활용되는 지 알아보기

SleepNightAdapter.kt

    override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
        val item = data[position]

        holder.textView.text = item.sleepQuality.toString()

        if (item.sleepQuality <= 1) {
            holder.textView.setTextColor(Color.RED) // red
        }
    }

sleep quality가 1 이하일 때만 홀더의 글자 색깔을 빨간색으로 바꿨는데 데이터를 계속 추가해보면 1 이하가 아닌 숫자에 대해서도 색깔이 빨간색으로 뜬다. 이를 통해 홀더가 재사용되었음을 알 수 있다.

 

if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}

else 문을 추가하면 재사용 여부와 상관 없이 글자 색깔을 올바르게 띄울 수 있다.

 

Create a ViewHolder for all the sleep data

Step 1: 아이템 레이아웃 생성하기

list_item_sleep_night.xml

<?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>

 

Step 2: ViewHolder 생성하기

SleepNightAdapter.kt

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        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)
    }

 

Step 3: SleepNightAdapter에서 ViewHolder 사용하기

Util.kt

private val ONE_MINUTE_MILLIS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES)
private val ONE_HOUR_MILLIS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)

fun convertDurationToFormatted(startTimeMilli: Long, endTimeMilli: Long, res: Resources): String {
    val durationMilli = endTimeMilli - startTimeMilli
    val weekdayString = SimpleDateFormat("EEEE", Locale.getDefault()).format(startTimeMilli)
    return when {
        durationMilli < ONE_MINUTE_MILLIS -> {
            val seconds = TimeUnit.SECONDS.convert(durationMilli, TimeUnit.MILLISECONDS)
            res.getString(R.string.seconds_length, seconds, weekdayString)
        }
        durationMilli < ONE_HOUR_MILLIS -> {
            val minutes = TimeUnit.MINUTES.convert(durationMilli, TimeUnit.MILLISECONDS)
            res.getString(R.string.minutes_length, minutes, weekdayString)
        }
        else -> {
            val hours = TimeUnit.HOURS.convert(durationMilli, TimeUnit.MILLISECONDS)
            res.getString(R.string.hours_length, hours, weekdayString)
        }
    }
}

 

SleepNightAdapter.kt

   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
        })
    }

 

Improve your code

Step 1: onBindViewHolder() 리팩토링 하기

코드를 리팩토링하고 모든 뷰 홀더 기능을 ViewHolder로 옮긴다.

SleepNightAdapter.kt의 onBindViewHolder()에서 변수 item 선언 부분을 제외한 모든 부분을 선택 후 우클릭 하고 [Refactor>Function]을 선택한다.

Name에 bind를 입력하고 OK를 누르면 다음과 같이 코드가 수정된다.

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = data[position]
        bind(holder, item)
    }

    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
        })
    }

bind()를 ViewHolder로 옮겨 코드를 다음과 같이 수정한다.

...
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = data[position]
        holder.bind(item)
    } 
...
    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        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)

        public 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
            })
        }
    }
...

 

Step 2: onCreateViewHolder 리팩토링 하기

inflation은 어댑터로 할 일이 아무것도 없고 모든 작업이 ViewHolder로 이루어진다. inflation은 ViewHolder에서 일어나야 한다.

 

onCreateViewHolder()의 body 전체를 선택하고 우클릭 후 [Refactor>Function]을 선택한다. Name을 from으로 하고 OK를 누른다.

from에 커서를 둔 후 [Alt+Enter]를 누르고 Move to companion object를 선택한다. from()은 companion object에 있어야 ViewHolder 인스턴스가 아닌 ViewHolder 클래스에서 호출될 수 있다. companion object를 ViewHolder 클래스 안으로 옮긴다.

...
    override fun onCreateViewHolder(parent: ViewGroup, viewType: 
Int): ViewHolder {
        return ViewHolder.from(parent)
    }
...
    class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
        ...
        companion object {
            public 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)
            }
        }
    }
...

ViewHolder 클래스의 생성자를 private으로 바꾼다. 이제 from()이 새로운 ViewHolder 인스턴스를 리턴하는 메서드이기 때문에 아무나 ViewHolder의 생성자를 호출할 이유가 없다.

 

Homework

Answer these questions

Q1. 1, 2, 4 / Q2. 1, 4 / Q3. 1, 2, 3 / Q4. 1, 3, 4