개발일지

Android in A..Z - RecyclerView (기본) 본문

Android (안드로이드)/RecyclerView

Android in A..Z - RecyclerView (기본)

강태종 2021. 1. 10. 00:48

RecyclerView

제한된 화면에 여러가지 데이터를 리스트 형식으로 표현하는 뷰이다.

ListView보다 성능이 좋으며 다양한 LayoutManager를 제공하므로 다양한 형식의 리스트로 데이터를 표현할 수 있다.

RecyclerView


Dependency

dependencies {
    implementation "androidx.recyclerview:recyclerview:1.1.0"
    // For control over item selection of both touch and mouse driven selection
    implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc03"
}

장점

  • DataBinding과 MVVM 패턴을 활용하여 다양한 데이터의 형식을 쉽게 표현할 수 있다.
  • 생성된 ViewHolder를 재사용하므로 리소스를 아낄 수 있다.
  • ItemDecoration, ItemAnimator 등 다양한 효과를 줄 수 있다.

구성

  • Adapter : 데이터 관리, Holder 관리, 데이터와 Holder를 연결해주는 역할을 한다.
  • ViewHolder : 데이터가 실제로 표시되는 객체이다.
  • LayoutManager : RecyclerView의 Layout을 관리한다.

ViewHolder

실제로 데이터가 표현되는 객체이며 여러개의 ViewHolder를 구현하여 RecyclerView에 다양한 데이터를 여러 종류의 형식으로 표현할 수 있다.

RecyclerView.ViewHolder를 상속받아서 구현한다.

=> 자신의 채팅을 표현하는 MyChatHolder, 상대방의 채팅을 표현하는 OtherChatHolder


BaseHolder

abstract class BaseHolder<VB: ViewDataBinding, E: Any>(protected val binding: VB) : RecyclerView.ViewHolder(binding.root) {
    val context: Context
        get() { return itemView.context }

    lateinit var element: E

    open fun bind(element: E) {
        this.element = element
    }
}

MyChatHolder

class MyChatHolder(binding: HolderMyChatBinding) : BaseHolder<HolderMyChatBinding, Chat>(binding) {
    override fun bind(element: Chat) {
        super.bind(element)
        binding.chat = element
    }
}

holder_my_chat.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="chat"
            type="com.taetae98.recyclerview.data.Chat" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_margin="5dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/message"
            style="@style/Chat"
            android:background="@color/chat_my"
            android:text="@{chat.text}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

OtherChatHolder

class OtherChatHolder(binding: HolderOtherChatBinding) : BaseHolder<HolderOtherChatBinding, Chat>(binding) {
    override fun bind(element: Chat) {
        super.bind(element)
        binding.chat = element
    }
}

holder_other_chat.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="chat"
            type="com.taetae98.recyclerview.data.Chat" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_margin="5dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/writer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{chat.writer}"
            app:layout_constraintStart_toEndOf="@+id/imageView"
            app:layout_constraintTop_toTopOf="parent" />

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:contentDescription="@string/profile"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_launcher_foreground" />

        <TextView
            style="@style/Chat"
            android:id="@+id/message"
            android:text="@{chat.text}"
            android:background="@color/chat_other"
            app:layout_constraintTop_toBottomOf="@id/writer"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/imageView" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Adapter

ViewHolder관리는 내부적으로 해주기 때문에 데이터에 따른 ViewHolder와 데이터만 관리해주면 된다.

ListAdapter을 상속받아서 구현한다. ListAdapter는 데이터의 타입, ViewHolder의 타입을 설정해야하고, ItemCallback을 생성자 매개변수로 받아야한다.

 

BaseAdapter

abstract class BaseAdapter<E: Any>(diffCallback: DiffUtil.ItemCallback<E>) : ListAdapter<E, BaseHolder<out ViewDataBinding, E>>(diffCallback) {
    override fun onBindViewHolder(holder: BaseHolder<out ViewDataBinding, E>, position: Int) {
        holder.bind(getItem(position))
    }
}

 

ChatAdapter

class ChatAdapter : BaseAdapter<Chat>(ChatDiffCallback()) {
    init {
        setHasStableIds(true)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseHolder<out ViewDataBinding, Chat> {
        return when(viewType) {
            R.layout.holder_my_chat -> {
                MyChatHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.context), viewType, parent, false))
            }
            R.layout.holder_other_chat -> {
                OtherChatHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.context), viewType, parent, false))
            }
            else -> {
                throw IllegalStateException("존재하지 않는 viewType : $viewType")
            }
        }
    }

    override fun getItemId(position: Int): Long {
        return getItem(position).id
    }

    override fun getItemViewType(position: Int): Int {
        return when(getItem(position).writer) {
            "My" -> {
                R.layout.holder_my_chat
            }
            else -> {
                R.layout.holder_other_chat
            }
        }
    }

    private class MyChatHolder(binding: HolderMyChatBinding) : BaseHolder<HolderMyChatBinding, Chat>(binding) {
        override fun bind(element: Chat) {
            super.bind(element)
            binding.chat = element
        }
    }

    private class OtherChatHolder(binding: HolderOtherChatBinding) : BaseHolder<HolderOtherChatBinding, Chat>(binding) {
        override fun bind(element: Chat) {
            super.bind(element)
            binding.chat = element
        }
    }

    private class ChatDiffCallback : DiffUtil.ItemCallback<Chat>() {
        override fun areItemsTheSame(oldItem: Chat, newItem: Chat): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Chat, newItem: Chat): Boolean {
            return oldItem.writer == newItem.writer && oldItem.text == newItem.text
        }
    }
}

 

  • onBindViewHolder : ViewHolder와 데이터가 연결되는 함수이다. ViewHolder에 데이터를 넘겨주고, ViewHolder에서 데이터에 맞게 View를 설정하면 된다. => 이미 생성된 ViewHolder를 재사용할 수도 있기 때문에 전에 데이터로 설정된 View도 다시 설정해야 한다.
  • onCreateViewHolder : ViewHolder를 생성하는 부분 ViewHolder를 재활용하지 않고 생성할 때 호출되는 함수. viewType에 맞게 ViewHolder를 생성하면 된다.
  • getItemId : 해당 item의 id를 얻을 때 호출되는 함수이다.
  • getItemViewType : 해당 item의 ViewType을 얻을 때 호출되는 함수이다. => 다양한 데이터를 다양한 형태의 ViewHolder로 표현할 때 사용한다.
  • submitList : 데이터 리스트를 adapter에 알려주는 함수이다.
  • notifyItem~ : 데이터의 변화를 알려주는 함수이다.

DiffUtil.ItemCallback

RecyclerView의 성능을 개선시킨 유틸리티로 기존의 데이터 리스트에서 업데이트할 데이터를 찾는다.

=> notifyDataSetChanged같은 데이터 변경 알림 함수를 사용할 때 모든 데이터를 ViewHolder에 업데이트하는 대신 변경된 데이터만 ViewHolder에 업데이트하면서 불필요한 리소스를 아낄 수 있다.

 

 

ChatDiffCallback

private class ChatDiffCallback : DiffUtil.ItemCallback<Chat>() {
    override fun areItemsTheSame(oldItem: Chat, newItem: Chat): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Chat, newItem: Chat): Boolean {
        return oldItem.writer == newItem.writer && oldItem.text == newItem.text
    }
}

 

  • areItemsTheSame : 해당 아이템이 같은 아이템인지 비교할 때 호출되는 함수이다.
  • areContentsTheSame : 해당 아이템의 내용이 같은지 확인할 때 호출되는 함수이다.
  • 처음에 areItemsTheSame 함수로 비교하고, areItemsTheSame에서 true를 반환하면 areContentsTheSame 함수를 호출하여 비교한다.

RecyclerView

ChatFragment

class ChatFragment : BaseFragment<FragmentChatBinding>(R.layout.fragment_chat) {
    private val adapter by lazy { ChatAdapter().apply {
        submitList(list)
    }}

    private val list by lazy { ArrayList<Chat>().apply {
        add(Chat(0, "Hi", "Other"))
        add(Chat(1, "Nice to meet you!!"))
    }}

    override fun init() {
        super.init()
        initRecyclerView()
        initSendButton()
    }

    private fun initRecyclerView() {
        binding.recyclerView.adapter = adapter
        binding.recyclerView.setHasFixedSize(true)
    }

    private fun initSendButton() {
        binding.setSendAction {
            list.add(Chat(list.size.toLong(), binding.message.text.toString(), "My"))
            adapter.notifyItemInserted(list.lastIndex)
            binding.message.text.clear()
        }
    }
}

fragment_chat.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="sendAction"
            type="android.view.View.OnClickListener" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@color/chat_background"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent">

            <EditText
                android:id="@+id/message"
                android:layout_width="0dp"
                android:minHeight="50dp"
                android:maxHeight="300dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:autofillHints=""
                android:background="@color/white"
                android:hint="@string/message"
                android:inputType="textMultiLine" />

            <ImageButton
                android:id="@+id/button"
                android:onClick="@{sendAction}"
                android:layout_width="50dp"
                android:src="@drawable/ic_send"
                android:layout_height="match_parent"
                android:background="@color/chat_send_box"
                android:contentDescription="@string/send" />
        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

Git (예제코드)

github.com/KangTaeJong98/Example/tree/main/Android/RecyclerView

 

KangTaeJong98/Example

My Example Code. Contribute to KangTaeJong98/Example development by creating an account on GitHub.

github.com

 

Comments