<코드랩-리사이클러뷰>
# 시작하며
이전에 화면의 표시되는 리스트들에 대해서, Utill을 사용해서 TextView를 나눠서 그려주게 했지만, 리사이클러뷰라는 강력한 기능을 이용해 각각을 표시하도록 개선했다. 리사이클러뷰는 이전에도 많이 사용해봤기 때문에 익숙했지만, 어떤게 어떤 기능을 하는지보다는 그냥 쓰라니까 썼었는데, 어댑터의 개념과 뷰홀더의 개념들을 다시 생각해 볼 수 있었다.
이 코드랩에서는 위와 같은 아키텍처가 사용 됐다. 뷰모델에서 비즈니스 로직과 데이터들을 관리하며, LiveData를 통해서 UI컨트롤러에 변화가 생겼을때 새로 업데이트 시켜주로고 헀다.
# 리사이클러뷰의 개념
앱에서 리스트의 데이터를 표현하는 경우는 매우 흔한 일이며 간단한 형식부터 복잡한 형식까지 매우 다양하다. 이러한 것을 쉽게 만들 수 있게 안드로이드에서는 RecyclerView라는 위젯을 제공한다.
리사이클러뷰를 사용하면 큰 리스트를 관리하는데 매우 효율적이다.
- 천개의 리스트를 관리해야하더라도, 리사이클러뷰를 사용하면 모든 리스트의 데이터를 관리하는 것이 아닌 화면에 보이는(예를들어 10개)리스트들만을 관리하면 된다. 화면을 스크롤할 시 화면에 표시되어야하는 새 리스트들을 파악하여 화면에 표시하도록 해준다.
- 스크롤되서 없어지는 리스트(뷰)라면 재활용된다. 즉 화면 맨 아래의 해당 뷰에 새로운 내용을 넣어서 보여주게 된다. 그래서 많은 연산시간을 절약하며 스크롤을 부드럽게 해준다.
- 한 아이템이 변화한다면, 리스트의 전체의 항목에 대해서 다시 그려주는 것이 아닌, 해당 아이템만을 update해주기 때문에 큰 효율성을 가질 수 있다.
# Adapter pattern (어댑터 패턴)
해외에 나갔을때 전원콘센트가 다 다른 것을 볼 수 있다. 220V를 사용하는 한국과 달리 일본만 하더라도 110V의 전원을 사용하기 때문에 플러그가 맞지 않는다. 이럴때 어댑터를 사용해서 해당 플러그에 맞도록 변환을 해주고는 한다. 하나의 인터페이스를 다른 인터페이스를 변환하는 작업이라고 볼 수 있다. 이러한 예와 비슷하게 소프트웨어 엔지니어링에서도 비슷한 개념의 어댑터 패턴을 사용한다. 한 클래스의 API를 다른 API로 사용 할 수 있도록 해준다.
리사이클러뷰에서 이러한 어댑터를 사용해 앱의 데이터(data)를 리사이클러뷰의 화면에 표시될 수 있도록 한다.
리사이클러뷰에는 뷰홀더(View Holder)라는 것이 존재한다. 뷰 홀더에는 레이아웃으로부터 하나의 아이템을 표현하기 위한 뷰의 정보를 가지고 있다.
# 리사이클러뷰 만들기 - ver.SIMPLE
리사이클러뷰를 만들기 위해서는, 각 아이템이 표시될 레이아웃이 필요하다. 또한 어댑터와 뷰홀더가 필요하다. 지금의 SIMPLE버전에서는 간단한 텍스트만을 리사이클러뷰를 통해 띄워 볼 것이다.
- fragment_sleep_tracker.xml의 TextView를 지우고 리사이클러뷰를 채워준다.
만들어진 리사이클러뷰에 LayoutManager속성에 LinearLayoutManager을 준다. 이 속성으로 각 아이템들이 수직-수평-그리드등 어떻게 표현될지를 알려준다.
- 이제 각각의 아이템이 표현될 레이아웃을 만들어주자. 현재는 간단히 텍스트만 표시할 것이기 때문에 TextView를 가진 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에 TextItemViewHolder 클래스를 만들어준다. 지금은 간단 버전이기 때문에 이렇게 만들고 뒤로가서 어댑터 안에서 Inner class로 ViewHolder를 정의할 것이다.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
- 이제 가장 중요한 어댑터를 만들어 준다. 어댑터는 뷰 홀더(View Holder)를 만들고 리사이클러뷰에 표현될 데이터를 채워주는 역할을 한다.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {
var data = listOf<SleepNight>()
}
SleepNightAdapter는 SleepNight객체를 리사이클러뷰에서 사용 가능하도록 변환시켜주는 역할을 할 것이다. data 변수를 만들어 어댑터가 관리하는 데이터를 만들어 주도록 한다. 이제 해당 클래스를 Implement하여 필수로 구현해야하느 인터페이스를 구현해주도록 하자.
class SleepNightAdapter : RecyclerView.Adapter<TextItemViewHolder>() {
var data = listOf<SleepNight>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {}
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {}
override fun getItemCount(): Int {}
}
- getItemCount는 list의 size를 반환해준다, 리사이클러뷰는 이로 인해 몇개의 데이터를 displaying하는지 알 수 있다.
override fun getItemCount() = data.size
- onBindViewHolder는 특정한 position의 item을 display하기 위해서 호출 된다. 또한 어떤 데이터들이 ViewHolder에 표시될지를 알려준다.
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
val item = data[position]
holder.textView.text = item.sleepQuality.toString()
}
- onCreateViewHolder는 리사이클러뷰가 ViewHolder가 필요할때 호출한다. parent의 인자는 viewholder를 쥐고있는(hold)하고 있는 view group인데 그렇기 때문에 항상 RecyclerView가 된다. viewType은 여러뷰가들이 같은 리사이클러뷰에 사용될때 쓰인다.
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)
}
layoutInflater를 통해 XML을 통해 만들어질 view를 가져와야 한다. context에는 view들에 대한 정보가 있다.
- 이제 리사이클러뷰에 data가 바뀔때마다 알려주기 위해서 data 변수에 대한 custom setter를 만들어 주어 변화할때마다 새로운 데이터를 다시 그리도록 해주자.
var data = listOf<SleepNight>()
set(value) {
field = value
notifyDataSetChanged()
}
notifyDataSetChanged()를 사용하면 리사이클러뷰는 변화된 아이템만을 다시 그리는 것이 아닌, 전체의 리스트를 다시 그린다. 지금은 이코드를 사용하지만 뒤로가서는 이 코드를 개선 시킬 것이다.
- 이제 SleepTrackerFragment에서 해다 어댑터에 데이털르 던져주어 리사이클러뷰에 표현되게 해주자.
val adapter = SleepNightAdapter()
binding.sleepList.adapter = adapter
어댑터 연결을 끝냈다면, DB에서 LiveData로 관리되고 있는 nights가 변화할때마다 리사이클러뷰에 데이터를 변경시켜줌으로써 리사이클러뷰가 데이터를 표현 할 수 있도록 해줘야한다.
- nights변수의 private를 없앤 후, nights변수가 변화할때마다, 리사이클러뷰가 다시 그려주도록 한다.
val nights = database.getAllNights()
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer { listofSleep ->
listofSleep?.let {
adapter.data = it
}
})
## 문제점
1이하에 숫자에만 빨간색으로 변화하게 해줬음에도 불구하고, 5라는 숫자에도 빨간색이 표시되는 문제점을 볼 수 있다.
if (item.sleepQuality <= 1) {
holder.textView.setTextColor(Color.RED) // red
}
이는 만들어진 뷰 홀더가 재사용되기 때문이다. 그렇기 때문에 모든 경우의 뷰홀더에 대해서 다시 설정하도록 해줘야한다.
if (item.sleepQuality <= 1) {
holder.textView.setTextColor(Color.RED) // red
} else {
// reset
holder.textView.setTextColor(Color.BLACK) // black
}
# 리사이클러뷰 만들기 - ver.SleepData
이제 뷰홀더를 조금 더 이쁘게 꾸밈과 동시에 여러 정보를 보여줄 수 있도록 코드를 개선해보도록 하자.
리사이클러뷰의 각각의 개체가 가질 item_layout을 새로 정의해주도록 하자. 레이아웃 코드는 생략하도록 하겠다.
## ViewHolder만들기
어댑터의 클래스 안에 ViewHolder를 만들어 주도록 한다. 그리고 각 뷰들에 대한 레퍼런스를 만들어주어 접근에 용이하도록 해준다.
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)
}
어댑터 내의 TextItemViewHolder가 쓰였던 부분을 ViewHolder로 치환하느 ㄴ과정과 onCreatViewHolder에서 참조하는 layout을 방금 생성한, list_item_sleep_night.xml로 바꿔주도록 한다.
이제 onBindViewHolder()에서 데이터들을 BInd시켜줘야 한다. Util을 사용해서 코드랩에서 제공해주는 것을 사용하면 보다 손쉽게 바꿔줄 수 있다.
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
})
}
viewholder에 만들어주었던 각각의 래퍼런스에 맞는 데이터를 Util에서 제공해주는 변환함수를 통해서 지정해주도록 하면 된다.
# Improve your code
코드를 개선 할 수 있다. onBindViewholder에서 각각의 데이터들을 bind시키는 것이 아닌 Viewholder에서 bind함수를 만들어서
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.bind(item)
}
과 같이 개선해줄 수 있다. 이 외에도 자잘한 몇가지가 있지만 생략하도록 하겠다.
# 마치며
리사이클러뷰를 구글이 제공해주는 자료를 통해서 공부해볼 수 있어서 좋았다. 뭔가 두루뭉실했던 것에 대해서 조금은 정립이 됐다. 다음 시간에는 DiffUtil을 사용한다고 하는데, 이를 통해서 특정 데이터만을 바꿀 수 있도록 해줄 것 같다. 이제 개강이다..ㅠ