카테고리 없음

Recycler View 중첩

cjeongmin 2022. 10. 4. 14:07

Recycler View

구글 공식 문서에서는 Recycler View를 아래와 같이 설명하고 있다.

RecyclerView를 사용하면 대량의 데이터 세트를 효율적으로 표시할 수 있습니다. 개발자가 데이터를 제공하고 각 항목의 모양을 정의하면 RecyclerView 라이브러리가 필요할 때 요소를 동적으로 생성합니다.
이름에서 알 수 있듯이 RecyclerView는 이러한 개별 요소를 재활용합니다. 항목이 스크롤되어 화면에서 벗어나더라도 RecyclerView는 뷰를 제거하지 않습니다. 대신 RecyclerView는 화면에서 스크롤된 새 항목의 뷰를 재사용합니다. 이렇게 뷰를 재사용하면 앱의 응답성을 개선하고 전력 소모를 줄이기 때문에 성능이 개선됩니다.

Recycler View 구현

일단 Recycler View 중첩을 구현하기 전에, 단일 Recycler View를 구현을 해보자.
Recycler View에는 뷰모델과, 어댑터가 필요하고, 개별 아이템을 나타낼 xml, Recycler View를 포함하거나 나타내는 xml이 필요하다.

간단한 Todo-List를 만들어 보자.

xml

<!-- MainActivity xml file -->
<?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="match_parent"  
    tools:context=".MainActivity">  

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        app:layout_constraintBottom_toBottomOf="parent"  
        app:layout_constraintEnd_toEndOf="parent"  
        app:layout_constraintStart_toStartOf="parent"  
        app:layout_constraintTop_toTopOf="parent"  
        tools:listitem="@layout/recyclerview_item" />  

</androidx.constraintlayout.widget.ConstraintLayout>
<!-- List Item xml file -->
<?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="wrap_content"  
    android:layout_height="wrap_content"  
    android:padding="16dp">  

    <TextView
        android:id="@+id/title"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:textStyle="bold"  
        app:layout_constraintStart_toStartOf="parent"  
        app:layout_constraintTop_toTopOf="parent"  
        tools:text="Title" />  

    <TextView 
        android:id="@+id/time"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        app:layout_constraintStart_toStartOf="parent"  
        app:layout_constraintTop_toBottomOf="@id/title"  
        tools:text="13:42" />  

    <View
        android:layout_width="match_parent"  
        android:layout_height="1dp"  
        android:background="#c3c3c3"  
        app:layout_constraintTop_toBottomOf="@id/time" />  
</androidx.constraintlayout.widget.ConstraintLayout>

간단한 Recycler View xml 파일들을 만들어 보았다.

kotlin

이젠 Recycler View와 연결할 어댑터와 뷰모델을 만들어야 하는데 코드는 아래와 같다.

package com.cjeongmin.viewbindingwithrecyclerview

import android.view.LayoutInflater  
import android.view.ViewGroup  
import androidx.recyclerview.widget.RecyclerView  
import com.cjeongmin.viewbindingwithrecyclerview.databinding.RecyclerviewItemBinding  

class MainAdapter(val taskList: List<Task>) : RecyclerView.Adapter<MainAdapter.MainViewHolder>() {  
    inner class MainViewHolder(val itemBinding: RecyclerviewItemBinding) :  
        RecyclerView.ViewHolder(itemBinding.root) {  
        fun bindItem(task: Task) {  
            itemBinding.title.text = task.title  
            itemBinding.time.text = task.timeStamp  
        }  
    }  

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {  
        return MainViewHolder(  
            RecyclerviewItemBinding.inflate(  
                LayoutInflater.from(parent.context),  
                parent,  
                false            
            )  
        )  
    }  

    override fun onBindViewHolder(holder: MainViewHolder, position: Int) {  
        val task = taskList[position]  
        holder.bindItem(task)  
    }  

    override fun getItemCount(): Int {  
        return taskList.size  
    }  
}

Adapter 안에 inner class로 ViewHolder를 만들고, 이를 아이템 xml 파일의 id 값들에 접근해서 인자로 전달받은 값으로 변경을 하고 있고, onBindViewHolder 함수에서 MainAdapter 생성자로부터 받은 taskList를 이용해 각 List의 요소마다 bindItem 함수를 호출하여 연결한다.

package com.cjeongmin.viewbindingwithrecyclerview  

import androidx.appcompat.app.AppCompatActivity  
import android.os.Bundle  
import androidx.recyclerview.widget.LinearLayoutManager  
import com.cjeongmin.viewbindingwithrecyclerview.databinding.ActivityMainBinding  

class MainActivity : AppCompatActivity() {  
    private var binding: ActivityMainBinding? = null  

    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  

        binding = ActivityMainBinding.inflate(layoutInflater)  
        setContentView(binding?.root)  

        val adapter = MainAdapter(Constants.taskList)  
        binding?.recycler_view?.layoutManager = LinearLayoutManager(  
            this,  
            LinearLayoutManager.VERTICAL,  
            false        
        )  
        binding?.recycler_view?.adapter = adapter  
    }  


    override fun onDestroy() {  
        super.onDestroy()  
        binding = null  
    }  
}

Recycler View를 사용하고 있는 곳에서 위에서 만든 adapter를 만들고, Recycler View에 접근해 adapter를 연결한다.
그리고 Recycler View의 요소 중 하나인 LayoutManager라는 것이 존재하는데 이는 RecyclerView 라이브러리에서는 아래와 같은 세가지 항목을 제공한다.

  1. LinearLayoutManager
  2. GridLayoutManager
  3. StaggeredGridLayoutManager

여기서는 1번의 LinearLayoutManager를 이용해 Layout을 설정하였다.
이러한 방식으로 단일 Recycler View를 만들 수 있다. 중첩 Recycler View는 어떻게 만들까?

Recycler View 중첩

일단, 결론부터 말하자면 상위 Recycler View의 ViewHolder에서도 Adapter를 만들어 하위 Recycler View와 연결하면 된다.
위를 예시로 들자면 위의 Activity 클래스에서 adapter를 만들어 Recycler View 요소에 접근해 Adapter, LayoutManager를 설정하는데, 이를 상위 Recycler View의 ViewHolder에서도 똑같이 구현해주면 된다.

참고 Github

<!-- activity_main.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="match_parent"  
    tools:context=".MainActivity">  

    <androidx.recyclerview.widget.RecyclerView  
        android:id="@+id/parent_recycler_view"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        tools:listitem="@layout/child_recycler_view"  
    />  

</androidx.constraintlayout.widget.ConstraintLayout>
<!-- child_recycler_view.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"  
    android:padding="10dp">  

    <TextView  
        android:id="@+id/theme"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        android:layout_marginTop="10dp"  
        app:layout_constraintBottom_toTopOf="@id/child_recycler_view"  
        app:layout_constraintTop_toTopOf="parent"  
        tools:text="Theme" />  

    <androidx.recyclerview.widget.RecyclerView  
        android:id="@+id/child_recycler_view"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        app:layout_constraintBottom_toBottomOf="parent"  
        app:layout_constraintTop_toBottomOf="@id/theme"  
        tools:listitem="@layout/recycler_view_item" />  

</androidx.constraintlayout.widget.ConstraintLayout>
<!-- recycler_view_item.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="wrap_content"  
    android:layout_height="wrap_content"  
    android:padding="16dp">  

    <TextView  
        android:id="@+id/title"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:textStyle="bold"  
        app:layout_constraintStart_toStartOf="parent"  
        app:layout_constraintTop_toTopOf="parent"  
        tools:text="Title" />  

    <TextView  
        android:id="@+id/time"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        app:layout_constraintStart_toStartOf="parent"  
        app:layout_constraintTop_toBottomOf="@id/title"  
        tools:text="13:42" />  

    <View  
        android:layout_width="match_parent"  
        android:layout_height="1dp"  
        android:background="#c3c3c3"  
        app:layout_constraintTop_toBottomOf="@id/time" />  

</androidx.constraintlayout.widget.ConstraintLayout>

xml 파일 구성이다. 이를 layout으로 설정하였다.

// 테스트로 사용할 더미 데이터 세트
package com.cjeongmin.nestedrecyclerviewwithviewbinding  

object Constant {  
    val todoList = arrayListOf<Todo>(  
        Todo(title = "Hello", "10:00"),  
        Todo(title = "World", "11:00"),  
        Todo(title = "Android", "13:00"),  
        Todo(title = "Recycle", "14:00"),  
        Todo(title = "View", "15:00")  
    )  

    fun getTodoByLength(maxLength: Int): ArrayList<Todo> {  
        val result = ArrayList<Todo>()  
        for (todo in todoList) {  
            if (todo.title.length == maxLength) {  
                result.add(todo)  
            }  
        }  
        return result  
    }  
}
// MainActivity.kt
package com.cjeongmin.nestedrecyclerviewwithviewbinding  

import androidx.appcompat.app.AppCompatActivity  
import android.os.Bundle  
import android.view.LayoutInflater  
import androidx.recyclerview.widget.LinearLayoutManager  
import com.cjeongmin.nestedrecyclerviewwithviewbinding.databinding.ActivityMainBinding  

class MainActivity : AppCompatActivity() {  
    private lateinit var binding: ActivityMainBinding  

    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        binding = ActivityMainBinding.inflate(layoutInflater)  
        setContentView(binding.root)  
        binding.parentRecyclerView.adapter = ParentRecyclerViewAdapter(arrayListOf("4", "5", "7"))  
        binding.parentRecyclerView.layoutManager = LinearLayoutManager(  
            binding.root.context,  
            LinearLayoutManager.VERTICAL,  
            false  
        )  
    }  
}
// ParentRecyclerViewAdapter.kt
package com.cjeongmin.nestedrecyclerviewwithviewbinding  

import android.view.LayoutInflater  
import android.view.ViewGroup  
import androidx.recyclerview.widget.LinearLayoutManager  
import androidx.recyclerview.widget.RecyclerView  
import com.cjeongmin.nestedrecyclerviewwithviewbinding.databinding.ChildRecyclerViewBinding  

class ParentRecyclerViewAdapter(private val themes: ArrayList<String>) :  
    RecyclerView.Adapter<ParentRecyclerViewAdapter.ViewHolder>() {  
    inner class ViewHolder(val itemBinding: ChildRecyclerViewBinding) :  
        RecyclerView.ViewHolder(itemBinding.root) {  
        fun bindItem(theme: String) {  
            itemBinding.theme.text = theme  
            val todoList = Constant.getTodoByLength(theme.toInt())  
            val childAdapter = ChildRecyclerViewAdapter(todoList)  
            itemBinding.childRecyclerView.adapter = childAdapter  
            itemBinding.childRecyclerView.layoutManager = LinearLayoutManager(  
                itemBinding.root.context,  
                LinearLayoutManager.VERTICAL,  
                false  
            )  
        }  
    }  

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {  
        return ViewHolder(  
            ChildRecyclerViewBinding.inflate(  
                LayoutInflater.from(  
                    parent.context,  
                ),  
                parent,  
                false  
            )  
        )  
    }  

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {  
        holder.bindItem(themes[position])  
    }  

    override fun getItemCount(): Int {  
        return themes.size  
    }  
}
// ChildRecyclerViewAdapter.t
package com.cjeongmin.nestedrecyclerviewwithviewbinding  

import android.view.LayoutInflater  
import android.view.ViewGroup  
import androidx.recyclerview.widget.RecyclerView  
import com.cjeongmin.nestedrecyclerviewwithviewbinding.databinding.RecyclerViewItemBinding  

class ChildRecyclerViewAdapter(private val todoList: ArrayList<Todo>) :  
    RecyclerView.Adapter<ChildRecyclerViewAdapter.ViewHolder>() {  
    inner class ViewHolder(private val itemBinding: RecyclerViewItemBinding) :  
        RecyclerView.ViewHolder(itemBinding.root) {  
        fun bindItem(todo: Todo) {  
            itemBinding.time.text = todo.time  
            itemBinding.title.text = todo.title  
        }  
    }  

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {  
        return ViewHolder(  
            RecyclerViewItemBinding.inflate(  
                LayoutInflater.from(parent.context),  
                parent,  
                false  
            )  
        )  
    }  

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {  
        holder.bindItem(todoList[position])  
    }  

    override fun getItemCount(): Int {  
        return todoList.size  
    }  

}

ParentRecyclerViewAdapter.kt 파일 코드에서 보면 bindItem 함수 내에서 Adapter를 생성하고 하위 Recycler View와 연결해주고 있다. MainActivity에서 ParentRecyclerView와 연결하듯이 상위 Recycler View에서 ViewHolder 클래스 내 bind함수에서 하위 Recycler View와 연결해주면 중첩 Recycler View를 구현할 수 있다.

 

실행 결과
위 코드 실행 결과