RecyclerView 아키텍쳐
리사이클러뷰 아키텍쳐를 만들기 위해선 LayoutManger, Adapter 와 ViewHolder 가 필수.
간단 설명 하자면
- LayoutManager : 레이아웃 매니저는 리사이클러뷰(뷰그룹, 컨테이너)에 보여질 뷰들이 어떻게 배치될 지 결정하는 옵션 - 그리드,버티컬,호라이즌 등
- Adapter : 어댑터는 리사이클러 아키텍쳐에서 가장 중요한 역할. 실제 data가 리사이클러뷰에서 보여질 수 있도록 ViewHolder연결. 리사이클러뷰에 데이터를 아이템 뷰로 치환하여 보여주는 역할.
- ViewHolder : 리사이클러 뷰에 보여질 뷰들을 ViewHolder에 연결.
위 3개의 요소가 모두 들어가야 한다.
RecyclerView의 특징
리사이클러뷰는 대용랑 List를 처리하기에 적합하다.
-현재 스크린에서 보여지고 있는 item만 처리한다.
-스크롤이 이루어 질 때 마다 모든 리스트를 새로 그리지 않는다.
-스크롤을 할 때 사용됐던 뷰를 재활용을 하면서, 자원 효율성을 극대화.
RecyclerView 생성 과정
- .xml 파일에 리사이클러뷰를 생성한다.
리사이클러뷰가 들어갈 위치에 리사이클러뷰를 생성한다.리사이클러뷰가 생성될 fragment_sleep_tracker.xml ConstraintLayout내에 리사이클러뷰가 생성된 모습. <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_constraintTop_toBottomOf="@+id/start_button" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/>
- 리사이클러뷰 내에 LayoutManger를 설정한다.
레이아웃 매니저는 리사이클러뷰 내부에 보여질 item들을 위한 layout과 infra를 제공한다.
<androidx.recyclerview.widget.RecyclerView ... app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" ... />
LinearLayoutManger 이외에도 GridLayoutManger 등 여러가지 Layout이 있다. - Adapter를 생성.
어댑터에 data를 넣을 ViewModel과 어댑터와 리사이클러뷰를 바인딩할 프래그먼트가 있는 package에 "~~Adapter" 클래스를 만든다.
※Adapter class 를 만들 때 주의할 점 :
-어댑터 클래스는 RecyclerView.Adapter를 상속한다.
-상속하는 RecyclerView.Adapter의 타입에 ViewHolder Class를 지정해줘야 한다.
-따라서 adapter와 함께 사용할 ViewHolder class를 만들어줘야 한다.
-해당 ViewHolder class는 보통 Adatper class안에서 만들고 ~~Adapter.ViewHolder를 RecyclerView.Adapter 타입으로 지정해준다.
class SleepNightAdapter : RecyclerView.Adapter<SleepNightAdapter.ViewHolder> { class ViewHolder(itemView: View) : RecycleView.ViewHolder(itemView) { } }
※ViewHolder class를 만드는 법에 대한 상세는 4번에서 계속.
위와 같은 방식으로 SleepNightAdapter를 만들고, 1개의 프로퍼티와 3가지의 메소드를 필수로 override해야 한다.
3-0. var data : List<SleepNight> = listOf()
var data = listOf<SleepNight>()
3-1. override fun getItemCount() : Int -> 해당 함수는 현재 어댑터가 가진 데이터의 개수를 반환한다. 리사이클러뷰 측에서 어댑터가 현재 얼마나 많은 데이터를 가지고 있는지 판단하기 위해 필요하다.
override fun getItemCount(): Int = data.size
3-2.override fun onBindView(holder:ViewHolder , position:Int) -> 해당 함수는 실제 data가 ViewHolder의 itemView에 연결될 수 있도록 개조하는 함수이다. 실제 data가 리사이클러뷰에 보여질 때 ViewHolder가 호출되는데 필요하다.
첫번째 인자 holder는 ViewHolder 클래스 타입이며, holder의 itemView에 data를 바인딩하기 위해 필요하다.
두번째 인자 position은 Int 타입이며, 리사이클러뷰에 보여질 위치에 따라 어떤 데이터가 보여질 지 선택하기 위해 필요하다.
3-3.override fun onCreateViewHolder(parent:ViewGroup, viewType:Int) : ViewHolder -> 해당 함수는 리사이클러뷰(=뷰그룹)과 ViewHolder를 만들기 위한 인자 (=itemView)를 inflate(생성)하여 리사이클러뷰에서 사용할 ViewHolder를 직접적으로 생성하는 함수이다.override fun onBindView(holder:ViewHolder, position:Int) { val item = data[position] holder.특정뷰.text = item.sleepQuality.toString() }
첫번째 인자 parent는 viewGroup을 의미하는데, recyceleView에서 이 viewGroup은 결국 리사이클러뷰 자체를 의미한다. (리사이클러뷰 내에 많은 뷰들이 속한다고 생각하면 된다.)두번째 인자 viewType은 솔직히 아직 잘모르겠따 ㅎviewGroup의 context를 통해 LayoutInflater을 얻어온다.
layoutInflater를 통해 R.id.~~viewholder에 가져올 뷰, 뷰그룹, false 를 inflate 해서 view를 가져와 버린다.
이렇게 가져온 view를 인자로 viewholder를 만들어 반환.
override fun onCreateViewHolder(parent:ViewGroup, viewType:Int) : ViewHolder{ val layoutInflater = LayourInflater.from(parent.context) val view = layoutInflater.inflate(R.id.~~~, parent, false) return ViewHolder(view) }
<?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"> <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_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="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_constraintTop_toTopOf="@+id/quality_image" app:layout_constraintStart_toEndOf="@+id/quality_image" app:layout_constraintEnd_toEndOf="parent" tools:text="Wednesday" /> <TextView android:id="@+id/quality_string" android:layout_width="0dp" android:layout_height="20dp" app:layout_constraintTop_toBottomOf="@+id/sleep_length" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="@+id/sleep_length" app:layout_constraintEnd_toEndOf="@+id/sleep_length" tools:text="Excellent!!!" /> </androidx.constraintlayout.widget.ConstraintLayout>
3-4.ViewHolder Class 생성.
class ViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) {}
뷰홀더 자체는 어디에 생성되든 상관없지만, adapter 내 inner class로 생성될 경우 유용하게 사용할 수 있다.
(권장.)
그리고 viewHolder내에서 itemView를 갖는 변수들이 필요하다. -> 이 후 뷰홀더의 itemView들에 data를 바인딩하기 위해서.
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) }
adapter의 역할 중 하나는 리사이클러뷰로 하여금 데이터가 변할 때 알게 해야 한다. RecyclerView는 오롯이 adapter가 제공하는 ViewHolder를 통해서 view를 보여주고 있기 때문에 혼자서는 데이터의 변화를 알아챌 수 없다.
따라서 data 프로퍼티의 setter를 커스텀하여 다음과 같은 코드를 구현한다. notifyDataSetChanged() 함수는 데이터가 변경됐을 때 리사이클러뷰로 하여금 모든 데이터 목록을 새로 그리게 한다.
val data : List<SleepNight> = listOf() set(value) { field = value notifyDataSetChanged() }
- 3-5.RecyclerView 내 data가 변화할 때.
- Adapter를 리사이클러 뷰와 바인딩하기.
어댑터 클래스를 만들었다면 해당 어댑터를 .xml 의 리사이클러뷰의 어댑터와 바인딩 해야 한다.
따라서 Fragment내에서 binding.recyclerView.adapter 에 adapter를 초기화한다.
val adapter = SleepNightAdapter() //sleepList 는 recyclerView Id binding.sleepList.adapter = adapter
- Adapter에 실제 데이터를 바인딩하기.
리사이클러뷰가 보여줄 데이터를 어댑터에 바인딩하기 위해서는 ViewModel과 Fragment에서 코드를 구현해야 한다.
In ViewModel, 실제 보여질 데이터는 public, LiveData로 설정되어야 한다.실제 쓰여질 nights 변수가 database.getAllNights 로 부터 LiveData&amp;lt;List&amp;lt;SleepNight&amp;gt;&amp;gt;를 초기화.
In Fragment, 실제 데이터를 Observer하여 해당 데이터의 값이 바뀔 때 마다 adapter의 data프로퍼티를 실제 데이터로 초기화한다.
//In Fragment, In onCreateView viewModel.nights.observe(viewLifecycleOwner, Observer{ //it 이 null이 아닐 때 it?.let{ adapter.data = it } }
RecyclerView Implement
리사이클러뷰를 좀 더 안전하고, 효율적으로 사용하기 위해 리팩토링하는 과정을 거친다.
In Adapter Class
- override fun onBindViewHolder(holder:ViewHolder, position:Int) {}
//수정 전 함수. override fun onBindViewHolder(holder:ViewHolder, position:Int) { val item = data[position] val res = itemView.context.resources holder.sleepLength.text =convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res) holderquality.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 } ) }
해당 함수에서 item 변수를 제외한 (모든 코드를 선택 -> 우클릭 -> 리팩터 -> function <name=bind> ) 과정을 거치면
override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = data[position] bind(holder,item) } private fun bind(holder:ViewHolder, item:SleepNight) { val res = 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 } ) }
private bind 함수가 만들어지고, 해당 함수를 onBindViewHolder 함수 내에서 사용할 수 있다.
여기서 더 bind함수를 ViewHolder class 내에 넣어 class method 를 만든다.
class ViewHolder에 bind함수를 만들면, bind 매개변수에 holder를 만들어서 넘겨줄 필요가 없다. class 프로퍼티를 그대로 사용하면 된다.
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) 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 } ) } }
이런식으로 bind함수를 뷰홀더 내에 만들면, onBindViewHolder 함수에서
이런식으로 간단하게 사용할 수 있다. override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = data[position] holder.bind(item) }
- onCreateViewHolder(parent:ViewGroup, viewType:Int) : ViewHolder{}
해당 함수는 ViewHolder에 인자로 넘길 itemView들을 inflate하여 만들고, ViewHolder를 만들어서 반환하는 함수이다.
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) }
해당 함수의 "모든 코드를 선택 -> 우클릭 -> 리팩터 -> function <name=from>" 과정을 거치면
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return from(parent) } private 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) }
private from 함수를 만들어 반환할 수 있는데, 이를 좀 더 발전시켜서 from함수를 ViewHolder내에 넣는다.
ViewHolder내에 compainon object로 from함수를 만들 경우, 더 이상 ViewHolder 클래스가 외부에서 쓰이는 일이 없기 때문에 private하게 사용될 수 있어 안전성 측면에서 유리하다.
compainon object {} 스코프 내에 from함수를 정의한다.
class ViewHolder private constructor(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) ... 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) } } }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder.from(parent) }
'안드로이드' 카테고리의 다른 글
[Kotlin/Android] RecyclerView (3) DataBinding & Adapter (0) | 2021.12.31 |
---|---|
[Kotlin/Android] RecyclerView (2) DiffUtil & ListAdapter (0) | 2021.12.31 |
[Kotlin/Android] view의 상태에 따라 TextColor 바꾸기 (0) | 2021.12.27 |
[Kotlin/Android] Snackbar 만들기. (간단) (0) | 2021.12.27 |
[Kotlin/Android] Navigation JetPack에서 safeArgs사용 시, kotlin 코드에서 Directions 사용 안될 때. (0) | 2021.12.27 |