RecyclerView + ListAdapter 로 채팅 뷰 구현하기 (여러 개의 ViewHolder)

dev_minoflower·2022년 4월 1일
0
post-thumbnail

구동 환경

  • 최소 sdk : 23
  • 현재 sdk : 30
  • 언어 : Kotlin

패스트캠퍼스 강의 중에 중고거래 앱을 만드면서 채팅방을 구현하는 실습이 있었다.
강의에서는 채팅을 보내는 사용자와 상관 없이 우측에만 채팅 기록을 뿌려줬기 때문에, 추가적으로 기능을 개선시키기 위해 실제 사용하고 있는 채팅 앱처럼 사용자: 우측, 상대방: 좌측으로 나타날 수 있게 구현하고 싶었다.



일단 해보자 - LayoutParams 이용

  1. Adapter의 인자로 현재 사용자의 id를 받아온다.
  2. 커스텀으로 만든 ViewHolder에서 인자로 받아온 id 와 bind된 id 가 일치할 경우, id만 숨긴다.
  3. 일치하지 않을 경우, ConstraintLayout.LayoutParams를 이용해서 일일히 뷰를 조정했다.

이를 구현하고 난 뒤에 초기의 RecyclerView에서는 정상적으로 작동했지만,
채팅을 보내고 나니 상대방의 채팅 기록의 아이디가 자꾸 사라지는 이슈가 발생했다.

차라리 2개의 XML 파일로 내가 보여주고 싶은 순간에 적절히 뿌려줄 순 없을까 라는 생각을 가득 안고 구글링의 힘을 빌렸다.



ViewHolder를 더 넣을 수 있다고?

정말 운좋게도 medium 에서 RecyclerView의 ViewHolder를 extend시켜서 구현시킨 자료가 있었다😂


아래 사이트가 출처 경로인데 자세한 설명과 다른 이슈를 가지고 있다면 꼭 들어가보자!

mulitple-view-holder 참고하기


코드를 도입하기 전에...

  1. 내 코드에는 Recyclerview.Adapter가 아닌 ListAdapter로 구현했지만 ListAdapter의 제네릭 타입 두 번째로 ViewHolder를 지정하기에 가능하다고 판단했다.

  2. onCreateViewHolder를 들여다보면 인자로 ViewGroup, viewType 이지만 대부분은 단일 XML로 RecyclerView를 구현해서 viewType을 사용할 필요가 없었다.



1. getItemViewType 메소드 생성 및 사용

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

이는 특정 리스트의 아이템에 type을 설정해주는 것과 같다.
반환형이 Int 이기 때문에 다음과 같이 초기화해준다.

companion object {    
        private const val MY_CHAT = 1
        private const val OTHER_CHAT = 2
    }
override fun getItemViewType(position: Int): Int {
        return if (auth == currentList[position].senderId)
            MY_CHAT
        else OTHER_CHAT
    }

auth는 어댑터에서 인자로 받아온 현재 사용자(나) id이다.
ListAdapter에서 제공하는 currentList을 이용해서 특정 뷰가 지정된 position의 senderId가 auth와 같다면 우측에 그려주겠다! 를 표시하는 것이고, 아니라면 상대방의 것을 좌측에 그려주겠다! 를 표시한다.



2. ViewHolder 2개 작성하기(나, 상대방)

onCreateViewHolder에 2개의 ViewHolder를 반환해야 한다.

inner class MyChatItemViewHolder(private val binding: ItemChatBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(chat: ChatItem) {
            val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
            val date = Date(chat.time)
            binding.messageTextView.text = chat.message
            binding.timeTextView.text = dateFormat.format(date)
        }
    }

inner class OtherChatItemViewHolder(private val binding: ItemOtherChatBinding) :
       	RecyclerView.ViewHolder(binding.root) {
        fun bind(chat: ChatItem) {
            val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
            val date = Date(chat.time)
            binding.senderTextView.text = chat.senderId
            binding.messageTextView.text = chat.message
            binding.timeTextView.text = dateFormat.format(date)
        }
    }

binding을 실습하고 있는 단계라 ViewBinding을 적용시켰다.
Adapter에서 사용하니 너무나도 깔끔해서 보기 좋다 👍🏻



3. onCreateViewHolder에서 viewType 이용하기

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if(viewType == MY_CHAT) {
            ChatItemViewHolder(
                ItemChatBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        } else {
            ChatItem2ViewHolder(
                ItemOtherChatBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        }
    }


4. onBindViewHolder 작성

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if(getItemViewType(position) == MY_CHAT) {
            (holder as MyChatItemViewHolder).bind(currentList[position])
        } else {
            (holder as OtherChatItemViewHolder).bind(currentList[position])
        }
    }

ViewHolder의 반환형RecylcerView.ViewHolder이므로 as 키워드를 통해 형 변환을 적용한다. 특정 뷰홀더를 넣을 시 다른 뷰홀더를 사용할 수 없게 되니 말이다.

위와 같이 형 변환으로 간단히 해결할 수 있었다!


c.f.) ListAdpater의 제네릭 타입

ListAdapter<ChatItem, RecyclerView.ViewHolder>(diffUtil)


회고

이번 실습을 혼자 응용해보면서 RecyclerView를 더 유용하고 확장성 있게 구현하는 방법을 알게 된 것 같다.

역시 스스로 개선 사항을 끊임 없이 고민해보고 더 좋은 방향이 있다면 혼자 힘으로 여러 방법을 시도해보고, 만약 안된다면 다른 방법들을 찾아보며 성장하는 것이 큰 도움이 되겠다는 생각이 들었다.


전체 코드

class ChatItemAdapter(
    private val auth: String
) : ListAdapter<ChatItem, RecyclerView.ViewHolder>(diffUtil) {
    inner class MyChatItemViewHolder(private val binding: ItemChatBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(chat: ChatItem) {
            val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
            val date = Date(chat.time)
            binding.messageTextView.text = chat.message
            binding.timeTextView.text = dateFormat.format(date)
        }
    }

    inner class OtherChatItemViewHolder(private val binding: ItemOtherChatBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(chat: ChatItem) {
            val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
            val date = Date(chat.time)
            binding.senderTextView.text = chat.senderId
            binding.messageTextView.text = chat.message
            binding.timeTextView.text = dateFormat.format(date)
        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if(viewType == MY_CHAT) {
            MyChatItemViewHolder(
                ItemChatBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        } else {
            OtherChatItemViewHolder(
                ItemOtherChatBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        }
    }
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if(getItemViewType(position) == MY_CHAT) {
            (holder as MyChatItemViewHolder).bind(currentList[position])
        } else {
            (holder as OtherChatItemViewHolder).bind(currentList[position])
        }
    }

    override fun getItemViewType(position: Int): Int {
        return if (auth == currentList[position].senderId)
            MY_CHAT
        else OTHER_CHAT
    }

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

            override fun areContentsTheSame(oldItem: ChatItem, newItem: ChatItem): Boolean {
                return oldItem == newItem
            }
        }
        private const val MY_CHAT = 1
        private const val OTHER_CHAT = 2
    }
}

실행화면

profile
성장하는 개발자가 되고 싶어요

0개의 댓글