기본 RecyclerView / 멀티 뷰 타입 RecyclerView

구성 요소

  • item : Display 할 여러 단어 중 하나의 Word object가 하나의 item에 해당한다.
  • Adapter : data를 RecyclerView 화면에 display할 준비를 한다.
  • ViewHolder : A pool of views. 리사이클러뷰가 display에 사용/재사용 하는 뷰


기본 RecyclerView

List에 있는 item들을 화면에 보여주는 가장 기본적인 RecyclerView를 만들어보자.

사진처럼 한 칸에 이미지 하나, 단어 하나가 들어가는 RecyclerView를 만들 것이다. (이미지는 안드로이드 스튜디오 기본 벡터 아이콘에서 아무거나 가져왔다 😅)

data class: Word.kt

Word라는 data class를 생성한다.

data class Word(
    @StringRes val stringResourceId: Int,
    @DrawableRes val imageResourceId: Int
    )
  • Word는 하나의 단어를 나타낸다.
  • Word 인스턴스를 만들 때 문자열과 이미지 리소스 ID를 전달해야 한다.
  • 문자열 리소스 ID, 이미지 리소스 ID 모두 Int값이기 떄문에 호출 시 잘못된 순서롤 전달 할 수 있다. 이를 방지하기 위해 리소스 주석을 사용할 수 있다. @StringRes, @DrawableRes를 설정하면 잘못된 유형의 리소스 ID가 들어오면 경고가 표시된다.

Datasource

화면에 표시 할 데이터는 외부 소스에서 가져올 수도 있지만 이번 실습에서는 drawablevalues > string 디렉토리에 준비 된 데이터를 가져다 사용할 것이다.

Datasource라는 클래스를 생성하고, 클래스 내부에 loadWords() 함수를 만든다.

class Datasource {
    fun loadWords(): List<Word>{
        return listOf<Word>(
            Word(R.string.word1, R.drawable.ic_1),
            Word(R.string.word2, R.drawable.ic_2),
            Word(R.string.word3, R.drawable.ic_3),
            Word(R.string.word4, R.drawable.ic_4),
            Word(R.string.word5, R.drawable.ic_5)
        )
    }
}
  • loadWords()는 Words 리스트를 반환한다.

activity_main.xml

리사이클러뷰를 표시할 액티비티의 xml은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
        android:scrollbars="vertical"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</FrameLayout>
  • 화면에 리사이클러뷰만 표시하므로 단일 하위 뷰를 갖는데 적합한 FrameLayout을 사용했다.
  • LayoutManager는 LinearLayourManger

list_item.xml

하나의 item 화면을 구성한다. 레이아웃 폴더에 추가했다.

<?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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            android:id="@+id/item_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingRight="20dp"
            tools:srcCompat="@drawable/ic_2" />

        <TextView
            android:id="@+id/item_word"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="@string/word1" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Adapter

class Adapter(
    private val context: Context, private val dataset: List<Word>
    ) : RecyclerView.Adapter<Adapter.ItemViewHolder>() {

    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val item_tv: TextView = view.findViewById(R.id.item_word)
        val item_iv: ImageView = view.findViewById(R.id.item_image)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val adapterLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.list_item, parent, false)
        return ItemViewHolder(adapterLayout)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = dataset[position]
        holder.item_tv.text = context.resources.getString(item.stringResourceId)
        holder.item_iv.setImageResource(item.imageResourceId)
    }

    override fun getItemCount(): Int {
        return dataset.size
    }
}
  • Adapter에는 Word 목록을 매개변수로 전달한다.
  • 문자열과 이미지 리소스를 확인하는 방법에 대한 정보나 앱 관련 정보는 Context 객체 인스턴스에 저장된다.
  • Adapter 클래스를 추상 클래스 RecyclerView.Adapter에서 확장하고, 꺾쇠 안에는 뷰 홀더 유형으로 Adapater.ItemViewHolder를 지정한다.

ㄴ ItemViewHolder

Adapter 클래스 내부에 ItemViewHolder 클래스를 생성한다. RecyclerView는 뷰와 직접 상호작용하는 것이 아니라 ViewHolder를 처리하기 때문.

    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val item_tv: TextView = view.findViewById(R.id.item_word)
        val item_iv: ImageView = view.findViewById(R.id.item_image)
    }

Adapter에서 Ctrl + I를 눌러 구현해야 하는 메서드를 생성할 수 있다.
onCreateViewHolder(), getItemCount(), onBindViewHolder()를 생성한다.

ㄴ onCreateViewHolder()

    override fun onCreateViewHolder(
        parent: ViewGroup, viewType: Int
    ): ItemViewHolder {
        val adapterLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.list_item, parent, false)
        return ItemViewHolder(adapterLayout)
    }
  • onCreateViewHolder()는 레이아웃 관리자가 새 뷰 홀더를 만들 때 호출한다. 재사용할 수 있는 기존 뷰 홀더가 없는 경우
  • parent 매개변수는 새 list item view가 child로 사용되는 상위 view group. 여기서 parent는 RecyclerView
  • viewType 매개변수는 여러개의 뷰 타입을 사용할 때 필요하다.
    LayoutInflater로 레이아웃 리소스 list_itemparent 뷰 그룹에 전달한다.
  • 마지막 매개변수 falseattachToRoot = false. 적절한 때에 리사이클러뷰가 이 item을 뷰 계층 구조에 추가하기 때문에.

ㄴ onBindViewHolder()

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = dataset[position]
        holder.item_tv.text = context.resources.getString(item.stringResourceId)
        holder.item_iv.setImageResource(item.imageResourceId)
    }
  • 레이아웃 관리자가 item 뷰의 컨텐츠를 바꾸기 위해 호출한다.
  • 매개변수로 전달 된 position을 기준으로 Word 객체를 찾는다.

RecyclerView를 표시하는 MainActivity

onCreate() 메서드에 다음 코드를 추가한다.

        val mDataset = Datasource().loadWords()
        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)

        recyclerView.adapter = Adapter(this, mDataset)
  • Datasource 인스턴스를 만들고 loadWords() 메서드를 호출한다.
  • recyclerView 변수에 레이아웃의 리사이클러뷰를 찾아 담는다.
  • Adapter 인스턴스를 생성해 recyclerView에게 알린다.


완성 🤩


멀티 뷰 타입 RecyclerView


위에서 만든 리사이클러뷰를 멀티 뷰 타입을 사용하도록 수정해보자. 채팅 화면처럼 오른쪽, 왼쪽 두 개의 뷰가 사용되도록 만든다.

list_item_right.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">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="20dp"
        android:gravity="right"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            android:id="@+id/item_right_image"
            android:paddingRight="20dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:srcCompat="@drawable/ic_2" />

        <TextView
            android:id="@+id/item_right_word"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:text="@string/word1" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Word.kt 수정

data class Word(
    @StringRes val stringResourceId: Int,
    @DrawableRes val imageResourceId: Int,
    val viewType: Int
){
    companion object {
        const val VIEW_TYPE_LEFT = 0
        const val VIEW_TYPE_RIGHT = 1
    }
}
  • Word 인스턴스를 만들 때 뷰 타입도 전달되도록 viewType 변수를 추가한다.
  • companion object에 두 뷰 타입을 작성한다.

Datasource 수정

이제 Word 하나 생성할 때 뷰 타입도 전달하기 때문에 각 Word의 마지막 매개변수에 0 또는 1을 추가한다.

class Datasource {
    fun loadWords(): List<Word>{
        return listOf<Word>(
            Word(R.string.word1, R.drawable.ic_1,0),
            Word(R.string.word2, R.drawable.ic_2,1),
            Word(R.string.word3, R.drawable.ic_3,0),
            Word(R.string.word4, R.drawable.ic_4,1),
            Word(R.string.word5, R.drawable.ic_5,0)
        )
    }
}
  • 0이 전달된 Word는 왼쪽에, 1이 전달된 Word는 오른쪽에 배치된다.

Adapter 수정

먼저 Adapter의 반환 유형을 다음과 같이 수정했다.

class Adapter(
    private val context: Context, private val dataset: List<Word>
    ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    . . .
}

ㄴ ItemViewHolder() 수정

    class ItemViewHolder1(private val view: View) : RecyclerView.ViewHolder(view) {
        val item_tv: TextView = view.findViewById(R.id.item_word)
        val item_iv: ImageView = view.findViewById(R.id.item_image)

    }
    class ItemViewHolder2(private val view: View) : RecyclerView.ViewHolder(view) {
        val item_tv2: TextView = view.findViewById(R.id.item_right_word)
        val item_iv2: ImageView = view.findViewById(R.id.item_right_image)
    }
  • 기존 view holder는 ItemViewHolder1로 수정하고, 오른쪽 뷰를 담당할 ItemViewHolder2를 추가했다.

ㄴ onCreateViewHolder() 수정

    override fun onCreateViewHolder(
        parent: ViewGroup, viewType: Int
    ): RecyclerView.ViewHolder {
        val adapterLayout: View?
        return when(viewType){
            Word.VIEW_TYPE_LEFT -> {
                adapterLayout = LayoutInflater.from(parent.context)
                    .inflate(R.layout.list_item, parent, false)
                ItemViewHolder1(adapterLayout)
            }
            Word.VIEW_TYPE_RIGHT -> {
                adapterLayout = LayoutInflater.from(parent.context)
                    .inflate(R.layout.list_item_right, parent, false)
                ItemViewHolder2(adapterLayout)
            }
            else -> throw RuntimeException("알 수 없는 뷰 타입")
        }
    }
  • 전달된 viewType에 따라 다른 뷰를 inflate하고, 뷰 타입에 맞는 뷰 홀더를 생성한다.

ㄴ onBindViewHolder() 수정

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = dataset[position]
        when(item.viewType){
            Word.VIEW_TYPE_LEFT -> {
                (holder as ItemViewHolder1).item_tv.text = context.resources.getString(item.stringResourceId)
                holder.item_iv.setImageResource(item.imageResourceId)
                holder.setIsRecyclable(false)
            }
            Word.VIEW_TYPE_RIGHT -> {
                (holder as ItemViewHolder2).item_tv2.text = context.resources.getString(item.stringResourceId)
                holder.item_iv2.setImageResource(item.imageResourceId)
                holder.setIsRecyclable(false)
            }
            else -> throw RuntimeException("알 수 없는 뷰 타입")
        }

    }

ㄴ getItemViewType() 추가

position 위치에 있는 Word 인스턴스의 viewType을 반환한다.

    override fun getItemViewType(position: Int): Int {
        return dataset[position].viewType
    }

Layout Backgroud

리사이클러뷰 칸이 잘 구분되어 보이게 색깔이 있는 상자에 Word를 넣었다.

layout_border.xml
이 xml 파일을 새로 생성하고, list_item.xmllist_item_right.xmlbackground 속성을 추가했다.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:radius="5dp"/>
    <solid
        android:color="#9DB1F6"/>
    <stroke
        android:width="3dp"
        android:color="@color/white"/>
</shape>


끝 😊


참고
Android Developers Codelab
멀티 뷰 타입 리사이클러뷰

좋은 웹페이지 즐겨찾기