안드로이드

[Kotlin/Android] RecyclerView (3) DataBinding & Adapter

란서 2021. 12. 31. 18:50

DataBinding & Adapter

 

  • Adapter와 DataBinding (with list_item)

    리사이클러뷰에 보여질 views = list_item.xml을 dataBinding 하도록 재구성한다.

    list_item_sleep_night.xml

    기존 root가 ConstraintLayout이었다면, layout 으로 바꾼 뒤, <data>와 <ConstraintLayout>으로 구분하여 재구성한다. -> 쉽게 alt+enter를 이용하여 바꿀 수 있다.

    <?xml version="1.0" encoding="utf-8"?>
    <layout 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">
    
        <data>
            <variable
                name="sleep"
                type="com.example.android.trackmysleepquality.database.SleepNight" />
    
        </data>
    
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
            <ImageView
                android:id="@+id/quality_image"
                .../>
    
            <TextView
                android:id="@+id/sleep_length"
                .../>
    
            <TextView
                android:id="@+id/quality_string"
                ... />
    
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>​


  • class Adapter 의 class ViewHolder 변경. - val binding 프로퍼티 사용 및 from()수정

    기존에는 ViewHolder 클래스에서 뷰를 참조하기 위해 findViewById() 함수를 이용하여 뷰와 data를 bind하였다.

    class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
    	val sleepLength : TextView = itemView.findViewById(R.id.sleepLength)
    	val sleepQuality : TextView = itemView.findViewById(R.id.sleepQuality)
    	val sleepImage : ImageView = itemView.findViewById(R.id.sleepImage)
        
     	fun bind(item: SleepNight) {
        	val res = itemView.context.resources
    
        	sleepLength.text = ~~~
            sleepQuality.text = ~~~
            sleepImage.setImageResource(~~)
        }   
    }

    binding을 이용한 ViewHolder class를 만들기 위해서는 companion object {} 내 from () 함수를 변경하면서 시작할  수도 있다. (순서 중요 않지만, from을 바꿔서 자연스럽게 바꾸기 가능)

    기존 from() 함수 
    //기존 from함수
     companion object { 
    	fun from(parent:ViewGroup) : ViewHolder{ 
    		val layoutInflater = LayoutInflater.from(parent.context)
            val view = layoutInfalter.inflate(R.id.list_item_sleep_night, parent, false)
            return ViewHolder(view)
    	} 
    }
    에서
            companion object {
                 fun from(parent: ViewGroup): ViewHolder {
                    val layoutInflater = LayoutInflater.from(parent.context)
                     val binding = ListItemSleepNightBinding.inflate(layoutInflater,parent,false)
    //                val view = layoutInflater.inflate(R.layout.list_item_sleep_night, parent, false)
                    return ViewHolder(binding)
                }
            }​

    으로 from함수를 binding 변수를 이용해 ViewHolder 클래스를 반환할 수 있도록 바꾸어 주었다.

    하지만 아직 ViewHolder 클래스는 매개변수로 View 타입의 변수를 받고 있기 때문에, 이를 변경해주도록 하자.

    //기존 class ViewHolder private constructor(itemView: View)  
    //: RecyclerView.ViewHolder(itemView) 
    
    
    //변경 
    class ViewHolder  private constructor(val binding: ListItemSleepNightBinding) 
    : RecyclerView.ViewHolder(binding.root) {...}
    기존 class에서는 View 타입의 매개변수를 받고, 이를 RecyclerView.ViewHolder의 인자로 넘겨주었지만

    변경된 class에서는 ListItme~~Binding 타입의 프로퍼티를 만들어 해당 프로퍼티를 초기화 한뒤, binding.root를 RecyclerView.ViewHolder의 인자로 넘겨주고 있다. 

    (RecyclerView.ViewHolder에서 필요한 매개변수가 View가 필요하나봄?)

    따라서 앞으로 ViewHolder class내부에서는 binding을 활용하여 코드 내에서 .xml의 코드를 수정 및 변경할 수 있다.
    class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root) {
    	//val sleepLength : TextView = itemView.findViewById(R.id.sleepLength)
    	//val sleepQuality : TextView = itemView.findViewById(R.id.sleepQuality)
    	//val sleepImage : ImageView = itemView.findViewById(R.id.sleepImage)
        
     	fun bind(item: SleepNight) {
        	val res = itemView.context.resources
    
        	binding.sleepLength.text = ~~~
            binding.sleepQuality.text = ~~~
            binding.sleepImage.setImageResource(~~)
        }   
    }
    다음과 같이 기본 프로퍼티들을 모두 없애주고, binding변수를 통하여 직접 .xml view들을 참조하는 모습.
    (굳이 프로퍼티를 생성하지 않고 Inline코드로 만들어주었다.)

  • lits_item_sleep_night.xml 코드의 데이터 바인딩 변수 sleep 을 이용하여 더 안전하고 간편한 코드 만들기.|

    SleepNightAdatper가 있는 package 내에 BindingUtils 를 만든다.

    sleeptracker/BindingUtils


    BindingUtils class 는 View 들의 확장함수로써 코드 내에서 다양한 타입의 뷰들을 다뤄 .xml 코드에서보다 좀 더 간편하고 가독성있게 뷰를 수정할 수 있게 만든다.

    이렇게 코드 내에서 textView확장함수를 사용하여 adapter와 xml코드가 Binding을 하기 위해서 
    어노테이션 @BindingAdapter() 이 필요하다. 인자로 .xml에서 사용할 이름을 String 타입으로 넣어주면 .xml에서 "app:이름" 으로 사용 가능하다.

    class BindingUtils {
        @BindingAdapter("sleepDurationFormatted")
        fun TextView.setSleepDurationFormatted(item: SleepNight) {
            text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
        }
    
        @BindingAdapter("sleepQualityString")
        fun TextView.setSleepQualityString(item: SleepNight) {
            text = convertNumericQualityToString(item.sleepQuality, context.resources)
        }
    
        @BindingAdapter("sleepImage")
        fun ImageView.setSleepImage(item: SleepNight) {
            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
                }
            )
        }
    }​
    BindingUtils 코드를 구현한 뒤, Adapter class로 돌아가 직접적으로 data와 view를 연결시키는 bind()함수를 수정한다.

    현재 bind() 함수는 binding을 사용하여 view를 참조하고 있지만, list_item_sleep_night.xml 코드의 데이터 바인딩 변수 sleep에 bind의 매개변수 item을 연결시켜주면 더 간편하게 binding을 이룰 수 있다.


            fun bind(item: SleepNight) {
                binding.sleep = item
                binding.executePendingBindings()
            }
     
    다음과 같이 binding.sleep= item을 초기화 하여준다. 

    binding.executePendingBindings()는 현재 binding되지 않은 보류된 view들(?)또한 빠르게 binding시켜주는 함수라 고 한다.(정확히 모름)

    이 후, .xml 코드에서 (viewmodel 처리 하듯이) 각각 view들의 text 또는 image들을 sleep 변수를 참조하여 사용할 수 있다.

    <?xml version="1.0" encoding="utf-8"?>
    <layout 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">
    
        <data>
            <variable
                name="sleep"
                type="com.example.android.trackmysleepquality.database.SleepNight" />
    
        </data>
    
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
            <ImageView
                android:id="@+id/quality_image"
      			...
                app:sleepImage="@{sleep}" />
    
            <TextView
                android:id="@+id/sleep_length"
              	...
                app:sleepDurationFormatted="@{sleep}" />
    
            <TextView
                android:id="@+id/quality_string"
    			...
                app:sleepQualityString="@{sleep}" />
    
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>​
     각각의 View 들이 BindingUtils 내에 구현된 확장함수들을 @BindingAdapter()의 도움으로 사용할 수 있게 되었다. (맞나?)


정리

 RecyclerView 아키텍쳐는 여러가지가 있다. 각자의 app상황에 따라 때로는 간편하게 구성할수 도 (그다지 크지 않은 데이터 목록일 때)  있으며, 때로는 복잡한 구조를 가질 수도 있다. (DataBinding과 large data list를 가질 때) 

 따라서 (1),(2),(3) 에 정리한 모든 아키텍쳐 구조들을 잘 습득해두고, 적절한 환경에 따라 리사이클러뷰를 구현하기 바란다.