<Android>RecyclerView

진섭·2023년 6월 15일
0

Android

목록 보기
18/31
post-thumbnail

🔨 RecyclerView란?

안녕하세요😃
오랜만에 TIL 이외에 RecyclerView에 대한 주제로 블로그에 출간하려 합니다.
RecyclerView는 이름처럼 화면을 스크롤이 될때 뷰를 파괴하지 않고 뷰를 재활용하여 새로운 아이템을 나타가게 해준다. 이러한 이유로 효율적인 메모리 관리가 가능하다.

🔨 RecyclerView 구성요소

RecyclerView를 사용하기 위해서는 ViewHolder, Adapter, LayoutManger로 구성이 되어 있다.

  • ViewHolder : 항목에 필요한 뷰 객체를 가진다. 즉, RecyclerView에서 아이템 뷰를 보관하고 해당 아이템 뷰의 구성 요소에 접근하는 객체입니다.
  • Adapter : 어댑터는 RecyclerView와 데이터 사이의 다리 역할를 하며 RecyclerView.Adapter 클래스를 상속하여 구현하며 이템 뷰의 생성, 데이터 바인딩, 데이터 변경 등을 관리한다. 쉽게 말해서 데이터를 받아서 리사이클러뷰에 표시하는 역할을 합니다.
  • LayoutManger : 어댑터로 만든 항목을 RecyclerView에 아이템 뷰를 배치하는 역할을 합니다.
    • LinearLayoutManager : 항목을 가로나 세로 방향으로 배치한다.

    • GridLayoutManager : 항목을 그리드로 배치한다.

    • StaggeredGridLayoutManager : 항목을 높이가 불규칙한 그리드로 배치한다.

이미지 출처 : https://recipes4dev.tistory.com/154

🔨 RecyclerView 만들어 보기

RecyclerView는 다음과 같은 순서를 가집니다.
1. activity_main에 RecyclerView를 추가합니다.
2. RecyclerView에 표시될 아이템뷰 레이아웃을 추가합니다.
3. 리사이클러뷰에 어댑터를 구현합니다.
4. 어댑터,레이아웃매니저를 지정합니다.

🔨 1. activity_main에 RecyclerView를 추가합니다.

아래와 같이 activity_main에 추가합니다.

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

🔨 2. RecyclerView에 표시될 아이템뷰 레이아웃을 추가합니다.

res -> layout에 RecyclerView가 표시될 아이템 뷰를 추가합니다.
저의 경우에는 item_acticle.xml로 생성하고 아래와 같이 코드를 추가했습니다.
아래와 같이 코드를 추가한 이유는 당근마켓 처럼 ImageView 옆에 TextView가 세로로 배치되는 걸 의도하고 싶어서 저렇게 아래와 같은 코드가 되었습니다.

item_acticle.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingStart="16dp"
    android:paddingTop="16dp"
    android:paddingEnd="16dp">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="110dp"
        android:layout_height="110dp"
        android:layout_marginBottom="16dp"
        android:scaleType="center"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tittleTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:maxLines="2"
        android:textColor="@color/black"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/imageView"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/dateTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@id/tittleTextView"
        app:layout_constraintTop_toBottomOf="@id/tittleTextView" />

    <TextView
        android:id="@+id/priceTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:textColor="@color/black"
        android:textSize="15sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@id/tittleTextView"
        app:layout_constraintTop_toBottomOf="@id/dateTextView" />

    <View
        android:layout_width="0dp"
        android:layout_height="1dp"
        android:background="@color/gray_cc"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

🔨 3. 리사이클러뷰에 어댑터를 구현합니다.

ViewHolder 준비하기

각 항목에 해당하는 뷰 객체를 가지는 ViewHolder는 RecyclerView.ViewHolder를 상속받는다.ViewHolder는 항목 레이아웃 XML 파일에 해당하는 binding 객체만 가지고 있으면 됩니다.

inner class ViewHolder(private val binding: ItemArticleBinding) :
        RecyclerView.ViewHolder(binding.root) {
       
    }

Adapter 준비하기

Recycler를 위한 Adapter는 RcyclerView.AdapterListAdapter를 상속을 받을 수 있는데 차이를 알아보자면

  • RcyclerView.Adapter : RcyclerView의 기본 Adapter 클래스이며 데이터의 변경이 발생할 때 직접 데이터 변경를 하여 UI를 업데이트를 해야 한다.
  • ListAdapter : RcyclerView.Adapter 클래스를 상속하며 데이터의 변경을 감지하며 자동으로 애니메이션 효과와 함께 UI를 업데이트를 하는 기능을 제공한다.

정리를 하자면 RcyclerView.Adapter는 데이터 변경을 수동으로 처리해야 하는 일반적인 Adapter를 만들고 ListAdapter는 데이터 변경에 대한 감지를 통해 처리할 수 있는 Adapter를 만들 수 있다.

저의 경우에는 DiffUtil를 통해 이전 데이터 세트와 새로운 데이터 세트 간의 차이를 계산하여 변경된 아이템만 업데이트하도록 하여 성능을 개선할 수 있는 장점을 취하기 위해 ListAdapter를 사용을 하였지만 RcyclerView.Adapter도 간략하게 정리를 하도록 하겠습니다.

RcyclerView.Adapter

이 예제 코드는 제가 예제로 만든 코드를 가져와 봤습니다.코드를 보면 AdapterViewHolder가 내부 클래스로 정의가 되어 있고 어댑터에 정의해야 하는 함수를 CustomAdapter 이 부분에서 빨간줄이 그어지면서 implement Members를 통해 3개의 함수를 정의하라고 합니다.

  • onCreateViewHolder : 항목의 뷰를 가지고 뷰 홀더를 준비하려고 자동으로 호출합니다.
  • getItemCount : 항목 개수를 판단하려고 자동으로 호출한다.
  • onBindViewHolder : 뷰 홀더의 뷰에 데이터를 출력하려고 자동으로 호출한다.
class CustomAdapter : RecyclerView.Adapter<CustomAdapter.AdapterViewHolder>() {
        inner class AdapterViewHolder(binding: ItemInfoBinding) :
            RecyclerView.ViewHolder(binding.root) {
            val nameTextView: TextView = binding.nameTextView
            val ageTextView: TextView = binding.ageTextView
            val korTextView: TextView = binding.korScoreTextView
            val deleteButton: Button =binding.Button

        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AdapterViewHolder {
            val infoBinding =
                ItemInfoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
            return AdapterViewHolder(infoBinding)
        }

        override fun getItemCount() = studentList.size

        override fun onBindViewHolder(holder: AdapterViewHolder, position: Int) {
            val item = studentList[position]
            holder.nameTextView.text = "이름 : ${item.name} "  // 이름은 첫 번째 요소
            holder.ageTextView.text = "나이 : ${item.age} "  // 나이는 두 번째 요소
            holder.korTextView.text = "국어 점수 : ${item.korScore} "  // 국어 점수는 세 번째 요소

            holder.deleteButton.setOnClickListener {
                studentList.removeAt(position)
                notifyItemRemoved(position)
                notifyItemRangeChanged(position, studentList.size)
            }
        }
    }

ListAdapter

이 예제 코드는 아까 당근마켓 처럼 보여주기 위해 item_acticle.xml에 아이템 뷰를 만들었던 예제에요.
여기서는 ListAdapter를 상속받고 DiffUtil을 사용하여 데이터의 변경 사항을 효율적으로 처리하기 위해 DiffUtil을 파라미터로 받았습니다. 여기서도 내부 클래스에 ViewHolder를 넣어 주었고 안에서 item_acticle에 넣어줄 제목,날짜,가격,사진을 처리하였습니다. 여기서 Glide는 이미지 로딩 라이브러리입니다. 사용한 이유는 나무위키에서 피카츄 사진을 가져오기 위해 사용을 했어요. ListAdapter에도 정의해야 하는 함수가 있는데

  • getItemCount() : RcyclerView.Adapter와 마찬가지로 리스트에 표시할 아이템 갯루를 반환하는 함수인데 ListAdapter는 자동으로 구현해주기 때문에 따로 정의할 필요가 없습니다.
  • onCreateViewHolder() : 아이템 뷰를 위한 뷰홀더 객체를 생성하고 반환하는 함수
  • onBindViewHolder() : 특정 위치의 아이템 데이터를 뷰홀더에 바인딩하여 표시하는 함수입니다.

마지막에는 diffUtil을 companion object로 만들어 ListAdapter에서 사용하기 위해 정확히는 ListAdapter의 생성자에 전달해주는 코드를 만들었습니다.
변수 diffUtil은 DiffUtil.ItemCallback의 익명 객체로서 아이템 비교를 담당합니다 ArticleModel는 데이터 클래스 파일입니다. diffUtil를 implement Members를 해주면 구현해야 하는 2개의 함수가 나타납니다.

  • areItemsTheSame : 함수는 두 개의 아이템이 동일한지 여부를 확인합니다

  • areContentsTheSame : 함수는 두 개의 아이템의 내용이 동일한지 여부를 확인합니다.

    정리를 하자면 areItemsTheSame는 아이템을 식별하는 함수이고 areContentsTheSame는 아이템의 내용 변경 여부를 판단하기 위해 사용한다.

class Adapter : ListAdapter<ArticleModel, Adapter.ViewHolder>(diffUtil) {
    inner class ViewHolder(private val binding: ItemArticleBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(articleModel: ArticleModel) {
   
            binding.tittleTextView.text = articleModel.tittle
            binding.dateTextView.text = "${articleModel.createtAt}월 01일"
            binding.priceTextView.text = articleModel.price

            if (articleModel.imageUrl.isNotEmpty()) {
                Glide.with(binding.imageView)
                    .load(articleModel.imageUrl)
                    .into(binding.imageView)
            }

        }
    }

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

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

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<ArticleModel>() {
            override fun areItemsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean {
                return oldItem.createtAt == newItem.createtAt
            }

            override fun areContentsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean {
                return oldItem == newItem
            }

        }
    }
}

🔨 어댑터,레이아웃매니저를 지정합니다.

Adapter와 layoutManager를 등록해 화면에 출력합니다.

RcyclerView.Adapter

class MainActivity : AppCompatActivity() {

  lateinit var binding: ActivityMainBinding
  val studentList = mutableListOf<Student>()
  lateinit var adapter: CustomAdapter

  override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
      binding = ActivityMainBinding.inflate(layoutInflater)
      setContentView(binding.root)

      adapter = CustomAdapter()

      recyclerView.adapter = adapter
      recyclerView.layoutManager = LinearLayoutManager(this@MainActivity)// layoutManager를 이용해 수직으로 배치하는 layoutManager를 설정한다.
      adapter.notifyDataSetChanged()//adapter에게 데이터가 변경되었음을 알려 업데이트된 내용을 반영하도록 한다.


🔨 ListAdapter

이 코드에서는 자신이 만든 Adapte를 변수에 넣고 그 변수에서 submitList() 메서드를 호출하여 데이터 리스트를 업데이트를 하였습니다 내용은 피카츄, 라이츄, 피츄를 나무위키에서 이미지 주소를 가져와 Gilde를 이용해 넣어줬어요. 양식은 dataClass 에 맞게 이름, 날짜(월만 넣어줬어요),가격,이미지Uri을 넣어줬습니다.

fragmentHomeBinding.recyclerView.layoutManager = LinearLayoutManager(context)는 RecyclerView의 레이아웃 매니저를 설정하는 코드입니다.
fragmentHomeBinding.recyclerView.adapter=adapte는 RecyclerView에 어댑터를 설정하는 코드이며 RecyclerView와 어댑터를 연결하여 RecyclerView가 어댑터의 데이터를 표시할 수 있도록 합니다.

private lateinit var adapter : Adapter // Adapter 타입의 adapter 선언

 adapter = Adapter() // 자신이 만든 Adapter를 넣어주기 
 adapter.submitList(mutableListOf<ArticleModel>().apply { // submitList() 메서드를 호출하여 데이터 리스트를 업데이트하는 부분
          add(ArticleModel("피카츄",1,"10000원","https://w7.pngwing.com/pngs/441/722/png-transparent-pikachu-thumbnail.png"))
          add(ArticleModel("라이츄",2,"15000원","https://static.wikia.nocookie.net/pokemon/images/9/92/%EC%A0%84%EC%A7%84%EC%9D%98_%EB%9D%BC%EC%9D%B4%EC%B8%84.png/revision/latest/scale-to-width-down/1200?cb=20220729043935&path-prefix=ko"))
          add(ArticleModel("피카츄",1,"10000원","https://w7.pngwing.com/pngs/441/722/png-transparent-pikachu-thumbnail.png"))
          add(ArticleModel("라이츄",2,"15000원","https://static.wikia.nocookie.net/pokemon/images/9/92/%EC%A0%84%EC%A7%84%EC%9D%98_%EB%9D%BC%EC%9D%B4%EC%B8%84.png/revision/latest/scale-to-width-down/1200?cb=20220729043935&path-prefix=ko"))
          add(ArticleModel("피츄",3,"22000원","https://i.namu.wiki/i/nOrOqNI0KKLacJSA8Dw_xttWqVR4theEDGtdyIUR2EBveCxx-7q5UkZYF63VWCArP91QgNVoCCPkyLCcUc79YA.webp"))
          add(ArticleModel("피카츄",1,"10000원","https://w7.pngwing.com/pngs/441/722/png-transparent-pikachu-thumbnail.png"))
          add(ArticleModel("라이츄",2,"15000원","https://static.wikia.nocookie.net/pokemon/images/9/92/%EC%A0%84%EC%A7%84%EC%9D%98_%EB%9D%BC%EC%9D%B4%EC%B8%84.png/revision/latest/scale-to-width-down/1200?cb=20220729043935&path-prefix=ko"))
          add(ArticleModel("피츄",3,"22000원","https://i.namu.wiki/i/nOrOqNI0KKLacJSA8Dw_xttWqVR4theEDGtdyIUR2EBveCxx-7q5UkZYF63VWCArP91QgNVoCCPkyLCcUc79YA.webp"))
          add(ArticleModel("피카츄",1,"10000원","https://w7.pngwing.com/pngs/441/722/png-transparent-pikachu-thumbnail.png"))
          add(ArticleModel("라이츄",2,"15000원","https://static.wikia.nocookie.net/pokemon/images/9/92/%EC%A0%84%EC%A7%84%EC%9D%98_%EB%9D%BC%EC%9D%B4%EC%B8%84.png/revision/latest/scale-to-width-down/1200?cb=20220729043935&path-prefix=ko"))
          add(ArticleModel("피츄",3,"22000원","https://i.namu.wiki/i/nOrOqNI0KKLacJSA8Dw_xttWqVR4theEDGtdyIUR2EBveCxx-7q5UkZYF63VWCArP91QgNVoCCPkyLCcUc79YA.webp"))
          add(ArticleModel("피츄",3,"22000원","https://i.namu.wiki/i/nOrOqNI0KKLacJSA8Dw_xttWqVR4theEDGtdyIUR2EBveCxx-7q5UkZYF63VWCArP91QgNVoCCPkyLCcUc79YA.webp"))
      })


 fragmentHomeBinding.recyclerView.layoutManager = LinearLayoutManager(context)
fragmentHomeBinding.recyclerView.adapter=adapter

완성된 화면

🔨 참고자료

  1. https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=ko
  2. https://recipes4dev.tistory.com/154
  3. https://gogigood.tistory.com/56
  4. https://velog.io/@haero_kim/Android-DiffUtil-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0
  5. https://wooooooak.github.io/android/2019/03/28/recycler_view/
  6. https://velog.io/@saint6839/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-RecyclerView
  7. https://velog.io/@appletorch/RecyclerView%EB%9E%80

코드

ListAdapter : https://github.com/kimjinsub1217/AndroidTraining/tree/main/Recyclerview
RcyclerView.Adapter : https://github.com/kimjinsub1217/App-SCHOOL-Unit-2-Kotlin-Example-of-studying-on-Android/tree/main/android35_ex02

profile
Android 개발자

0개의 댓글