RecyclerView
RecyclerView 구현하기
Implement RecyclerView and an Adapter
Step 1: LayoutManager와 RecyclerView 추가하기
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
'안드로이드 Android > Android Study Jams' 카테고리의 다른 글
8. Connect to the internet - Load and display images from the internet (0) | 2021.01.28 |
---|---|
8. Connect to the internet - Get data from the internet (0) | 2021.01.28 |
7. Databases and RecyclerView - Use LiveData to control Button states (0) | 2021.01.21 |
7. Databases and RecyclerView - Use coroutines with Room (0) | 2021.01.20 |
7. Databases and RecyclerView - Create a Room database (0) | 2021.01.18 |