Multi-View RecyclerView / Kotlin

Lee Jung-hwan·2023년 7월 15일
0

Android

목록 보기
3/4

주의! 이글은 개인 공부 목적으로 작성된 포스팅입니다.
잘못된 내용이 있을 수 있음을 주의해주세요.

오늘은 Multi-View RecyclerView에 대해 배워볼까 한다.
기존 리사이클러뷰를 커스텀 하게 만든다면 하나의 레이아웃 파일을 작성하고 그에 맞게 바인딩해 사용했다.

하지만, 여러 앱을 실행해보면 스크롤이 되면서 여러 항목이 보이는 앱을 접한 적이 있을 것이다.
첫 번째 칸에는 광고, 두 번째 칸에는 음식 선택 등등 이런 화면을 어떻게 구현 할까?

여러 방법이 있겠지만 그중 Multi-View RecyclerView를 활용하면 쉽게 구현할 수 있다.


😂 자세하게 설명해줘!

좀 더 쉽게 Multi-View에 대해 설명해보자면 아래 코드를 살펴보자.

	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        TODO("Not yet implemented")
    }

    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        TODO("Not yet implemented")
    }

우리가 보통 RecyclerView.Adpater<>를 상속받아 어댑터 클래스를 구현하면
기본적으로 위 3개의 코드를 override해서 구현해야 한다.

그럼 우리는 onCreateViewHolder()함수가 일반적으로 먼저 작동하는 걸 알 수 있을 거다.
그럼 매개 변수를 확인해보자.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)

위 코드에서 viewType은 뭘 까?
일반적으로 viewType은 따로 설정해주지 않으면 0이 리턴된다.


⭐️ getItemViewType(position : Int)?

이번 포스팅의 핵심 기능을 담당해줄 메소드이다.
그렇다고 엄청 어려운 건 아니니 천천히 이해해보자.

	override fun getItemViewType(position: Int): Int {
        return super.getItemViewType(position)
    }

해당 메소드를 override 하면 위와 같이 코드가 하나 생성되는데 해당 return 부분을 아래와 같이 변경해보자.

	override fun getItemViewType(position: Int): Int {
        return position
    }

위 코드처럼 작성할 경우 리사이클러뷰안에 아이템이 그려지는 순서대로 Postion이 반환된다.
예를 들어 사이즈가 3인 리사이클러뷰에 0번째 아이템이 그려지면 0이 리턴되고, 1번째 아이템이 그려지면 1이 리턴된다.


1️⃣ 호출 순서?

하지만 우리는 의문점을 품을 수 있다.
onCreateViewHolder() 메소드가 있는데 getItemViewType()은 언제 호출되는 거지?

정답은 getItemViewType()이 먼저 호출되고 viewType을 리턴해준다.
그 이후 onCreateViewHolder()가 실행된다.

기본적으로 getItemViewType()은 우리가 선언하지 않으면 항상 0을 리턴해준다는 점을 생각하면 이해하기가 쉽다.


⚒️ 구현해보자!

이제 직접 구현해보며 Multi-View에 대해 이해해보자.

제일 먼저 CustomAdapter 클래스를 만들어보자.

class CustomAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        TODO("Not yet implemented")
    }

    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        TODO("Not yet implemented")
    }

    override fun getItemViewType(position: Int): Int {
        return position
    }
}

리사이클러뷰에 대해 잘 이해가 안 되는 분은 RecyclerView.Adapter<RecyclerView.ViewHolder>()
부분이 이해가 안 될 수도 있다.

해당 코드의 원형을 보자.

RecyclerView.Adapater<VH>

위와 같이 적혀있다. 여기서 VH는 ViewHolder를 뜻한다.
즉, 위와 같이 선언하면 RecyclerView.ViewHolder 종류를 상속받아 다양한 어댑터로 재활용이 가능하다.


🏖️ 커스텀 레이아웃 만들기

제일 먼저 리사이클러뷰안에 사용할 레이아웃 3개를 만들어보자.

이미지 뷰가 있는 레이아웃 하나와 버튼이 존재하는 레이아웃 하나 텍스트 뷰가 존재하는 레이아웃 하나
총 3개를 제작했다.

  • custom_lay.xml - 버튼 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button" />
</LinearLayout>
  • custom_lay2.xml - 이미지뷰 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/img"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        tools:srcCompat="@tools:sample/avatars" />
</LinearLayout>
  • custom_lay3.xml - 텍스트뷰 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textSize="20sp"
        android:gravity="center"
        android:text="else 문이 작동한 리사이클러뷰 아이템입니다." />
</LinearLayout>

⚙️ companion object 선언

companion object는 우리가 일반적으로 val 변수로 선언했을 때와 달리 컴파일 타임에 상수로 선언된다.
반면 val로 선언하면 런타임 타임에 상수로 선언되어 companion object로 선언하는 걸 추천한다.

companion object{
        const val IMAGE_VIEW_TYPE = 0
        const val BUTTON_VIEW_TYPE = 1
    }

🪧 onCreateViewHolder 분기 처리

자 이제 기본적인 작업은 끝났다.
onCreateViewHolder()에서 viewType에 따른 view 생성을 정의해보자.

	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when(viewType){
            IMAGE_VIEW_TYPE -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.custom_lay2,parent,false)
                ImageViewHolder(view)
            }
            BUTTON_VIEW_TYPE -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.custom_lay,parent,false)
                ButtonViewHolder(view)
            }

            else -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.custom_lay3,parent,false)
                TextViewHolder(view)
            }
        }
    }

깔끔한 작성을 위해 when()을 사용해서 viewType이 리턴되면 해당 값을 사용해 companion object 안에 선언한 상수와 비교하여 레이아웃을 정의한다.

만약 위와 같이 할 경우 0번에는 이미지뷰 홀더 / 1번에는 버튼 뷰 홀더 / 나머지는 TextView 홀더가 작동한다.


🎨 innear class ViewHolder를 작성해보자.

위에 onCreateViewHolder()안에 작성한 뷰 홀더들의 함수를 작성해보자.

	inner class ImageViewHolder(view : View) : RecyclerView.ViewHolder(view){
        val imageView = view.findViewById<ImageView>(R.id.imageView)

        fun bind(){
            imageView.setImageResource(R.drawable.ic_android_black_24dp)
        }

    }

    inner class ButtonViewHolder(view : View) : RecyclerView.ViewHolder(view){
        val Button = view.findViewById<Button>(R.id.button)

        fun bind(){
            Button.setOnClickListener {
                Toast.makeText(it.context, "버튼이 클릭됐습니다." , Toast.LENGTH_SHORT).show()
            }
        }
    }


    inner class TextViewHolder(view : View) : RecyclerView.ViewHolder(view){
        val textView = view.findViewById<TextView>(R.id.textView)

        fun bind(){
            textView.text = "Else문이 작동해 생성된 TextView 입니다."
        }
    }

위와 같이 여러 개의 뷰 홀더를 정의해주면 상황에 맞게 사용할 수 있게 된다.


📞 bind() 함수를 호출해보자.

bind() 함수를 호출하기 위해서는 넘어온 holder를 적절하게 타입 캐스팅 해줘야 한다.

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when(position){
            IMAGE_VIEW_TYPE -> (holder as ImageViewHolder).bind()
            
            BUTTON_VIEW_TYPE -> (holder as ButtonViewHolder).bind()

            else -> (holder as TextViewHolder).bind()
        }
    }

💯 임의로 개수를 지정해서 출력해보자!

리사이클러뷰를 구현한 사람이라면 getItemCount() 함수가 어떤 역할을 하는지 알고 있을 것이다.
해당 개수를 임의로 지정하면 해당 개수 만큼 리사이클러뷰의 아이템이 그려진다.

테스트를 위해 10으로 지정해보자.

	override fun getItemCount(): Int {
        return 10
    }

🏗️ MainActivity를 수정해보자!

이제 진짜 마지막이다.
MainActivity에서 우리가 작성한 어댑터를 리사이클러뷰에 장착해보자!

	binding.recycler.layoutManager = LinearLayoutManager(this)
    binding.recycler.adapter = CustomAdapter()

😆 결과


긴 글 읽어주셔 감사하고 피드백은 언제나 환영입니다.
감사합니다.

profile
안녕하세요😁 안드로이드 개발자 이정환 입니다~⭐️

0개의 댓글