안드로이드

[Kotlin/Android] RecyclerView (1) LayoutManager, Adapter, ViewHolder

란서 2021. 12. 30. 00:35

RecyclerView 아키텍쳐

리사이클러뷰 아키텍쳐를 만들기 위해선 LayoutManger, Adapter 와 ViewHolder 가 필수.

 

 

리사이클러뷰 아키텍쳐

간단 설명 하자면

 

  • LayoutManager : 레이아웃 매니저는 리사이클러뷰(뷰그룹, 컨테이너)에 보여질 뷰들이 어떻게 배치될 지 결정하는 옵션 - 그리드,버티컬,호라이즌 등

  • Adapter : 어댑터는 리사이클러 아키텍쳐에서 가장 중요한 역할. 실제 data가 리사이클러뷰에서 보여질 수 있도록 ViewHolder연결. 리사이클러뷰에 데이터를 아이템 뷰로 치환하여 보여주는 역할.

  • ViewHolder : 리사이클러 뷰에 보여질 뷰들을 ViewHolder에 연결.

위 3개의 요소가 모두 들어가야 한다.

 

 

RecyclerView의 특징

 

리사이클러뷰는 대용랑 List를 처리하기에 적합하다.

-현재 스크린에서 보여지고 있는 item만 처리한다.

-스크롤이 이루어 질 때 마다 모든 리스트를 새로 그리지 않는다.

-스크롤을 할 때 사용됐던 뷰를 재활용을 하면서, 자원 효율성을 극대화.

 

리사이클러뷰의 뷰 재활용

 

RecyclerView 생성 과정

 

  1. .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"/>​


  2. 리사이클러뷰 내에 LayoutManger를 설정한다.

    레이아웃 매니저는 리사이클러뷰 내부에 보여질 item들을 위한 layout과 infra를 제공한다.

            <androidx.recyclerview.widget.RecyclerView
                ...
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                ...
                />​

    LinearLayoutManger 이외에도 GridLayoutManger 등 여러가지 Layout이 있다.


  3. 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 타입이며, 리사이클러뷰에 보여질 위치에 따라 어떤 데이터가 보여질 지 선택하기 위해 필요하다.

    override fun onBindView(holder:ViewHolder, position:Int) {
    	val item = data[position]
        holder.특정뷰.text = item.sleepQuality.toString()
    }​
    3-3.override fun onCreateViewHolder(parent:ViewGroup, viewType:Int) : ViewHolder -> 해당 함수는 리사이클러뷰(=뷰그룹)과 ViewHolder를 만들기 위한 인자 (=itemView)를 inflate(생성)하여 리사이클러뷰에서 사용할 ViewHolder를 직접적으로 생성하는 함수이다.

    첫번째 인자 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()
        }​
  4. 3-5.RecyclerView 내 data가 변화할 때.
  5. Adapter를 리사이클러 뷰와 바인딩하기.

    어댑터 클래스를 만들었다면 해당 어댑터를 .xml 의 리사이클러뷰의 어댑터와 바인딩 해야 한다.
    따라서 Fragment내에서 binding.recyclerView.adapter 에 adapter를 초기화한다.

    val adapter = SleepNightAdapter()
    
    //sleepList 는 recyclerView Id
    binding.sleepList.adapter = adapter​


  6. Adapter에 실제 데이터를 바인딩하기.

    리사이클러뷰가 보여줄 데이터를 어댑터에 바인딩하기 위해서는 ViewModel과 Fragment에서 코드를 구현해야 한다.



    In ViewModel, 실제 보여질 데이터는 public, LiveData로 설정되어야 한다.
    실제 쓰여질 nights 변수가 database.getAllNights 로 부터 LiveData&amp;amp;lt;List&amp;amp;lt;SleepNight&amp;amp;gt;&amp;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

  1. 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 함수에서
    이런식으로 간단하게 사용할 수 있다.



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