구동 환경
- 최소 sdk : 23
- 현재 sdk : 30
- 언어 : Kotlin
패스트캠퍼스 강의 중에 중고거래 앱을 만드면서 채팅방을 구현하는 실습이 있었다.
강의에서는 채팅을 보내는 사용자와 상관 없이 우측에만 채팅 기록을 뿌려줬기 때문에, 추가적으로 기능을 개선시키기 위해 실제 사용하고 있는 채팅 앱처럼 사용자: 우측, 상대방: 좌측으로 나타날 수 있게 구현하고 싶었다.
이를 구현하고 난 뒤에 초기의 RecyclerView에서는 정상적으로 작동했지만,
채팅을 보내고 나니 상대방의 채팅 기록의 아이디가 자꾸 사라지는 이슈가 발생했다.
차라리 2개의 XML 파일로 내가 보여주고 싶은 순간에 적절히 뿌려줄 순 없을까 라는 생각을 가득 안고 구글링의 힘을 빌렸다.
정말 운좋게도 medium 에서 RecyclerView의 ViewHolder를 extend시켜서 구현시킨 자료가 있었다😂
아래 사이트가 출처 경로인데 자세한 설명과 다른 이슈를 가지고 있다면 꼭 들어가보자!
내 코드에는 Recyclerview.Adapter
가 아닌 ListAdapter
로 구현했지만 ListAdapter의 제네릭 타입 두 번째로 ViewHolder를 지정하기에 가능하다고 판단했다.
onCreateViewHolder를 들여다보면 인자로 ViewGroup, viewType 이지만 대부분은 단일 XML로 RecyclerView를 구현해서 viewType을 사용할 필요가 없었다.
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와 같다면 우측에 그려주겠다! 를 표시하는 것이고, 아니라면 상대방의 것을 좌측에 그려주겠다! 를 표시한다.
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에서 사용하니 너무나도 깔끔해서 보기 좋다 👍🏻
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))
}
}
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
키워드를 통해 형 변환을 적용한다. 특정 뷰홀더를 넣을 시 다른 뷰홀더를 사용할 수 없게 되니 말이다.
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
}
}