[Background]
저번글에 이어서 이번에는 실제로 어떻게 화면을 커스터마이즈 할지 하나씩 확인해 보겠습니다. UIKit에 적용되어 있는 화면이 정말로 많은데 그 모든 것들을 다 정리할 수는 없고 같은 Chat SDK을 사용하다 보니깐 로직이 비슷한 점이 있습니다. 저는 가장 많이 사용되는 채팅방, 채팅리스트 같은 화면들을 커스텀하는 방법에 대해서 다뤄보겠습니다.
이번 글에서는 제가 생각했을때 샌드버드를 사용할 때 가장 처음 보는 화면인 채팅 리스트(channel List)에 대해서 한번 다뤄보겠습니다
내용들이 어느정도 처음 정리 글과 연과 되어 있으니 모르는 용어가 있다면 한번 확인해 보시면 좋을 것 같습니다.
https://jcodingcraft.tistory.com/2
이 글에 사용된 버전들
- Java 17
- Gradle 8.0.2
- kotlin 1.8.20
- SendBird UIKIt 3.8
[ChannelList 구성]
ChannelListActivity -> ChannelListFragment -> ChannelListModule 이렇게 이어지고
이 안에 총 3개의 Component으로 나눠집니다.
처음 화면으로 보면 Component 두개가 보입니다.
HeaderComponent -> 채팅리스트의 toolbar 부분
ChannelListComponent -> 현재 사용자의 채팅들을 RecyclerView로 통해서 보여줍니다.
현재 화면에는 없지만 마지막으로 StatusComponent가 있는데, 이 Component는 다른 화면에서도 쓰입니다. 현재 채팅 리스트에서 이 Component의 역할은 로딩중일 때 아니면 채팅방을 불러오지 못했을 때 띄워주는 화면입니다. (원래는 No Channel이라고 뜨지만, 커스터마이즈 했습니다)
결론부터 말하자면, 채팅 리스트는 이 세개만 수정만 하면 기존에 있던 화면으로부터 벗어나 저희가 원하는 화면으로 바꿀 수 있습니다.
[커스터마이즈]
원래 샌드버드에서 제공하는 화면은 왼쪽인데 한번 오른쪽으로 바꿔보겠습니다
일단은 HeaderComponent부터 바꿔볼건데요 저번 글에서 바꾸는 방법이 있지만 처음부터 다시 해보겠습니다. layout 파일은 github에서 확인 부탁 드립니다.
class CustomHeader : HeaderComponent() {
var search: View.OnClickListener? = null
var add: View.OnClickListener? = null
override fun onCreateView(
context: Context,
inflater: LayoutInflater,
parent: ViewGroup,
args: Bundle?,
): View {
val binding = CustomHeaderBinding.inflate(inflater, null, false)
binding.btnSearch.setOnClickListener {
search?.onClick(it)
}
binding.btnAdd.setOnClickListener {
add?.onClick(it)
// onRightButtonClicked(it)
}
return binding.root
}
}
CustomHeader class를 만들어서 HeaderComponent를 상속받습니다.
제가 만든 CustomHeader에는 버튼이 두개가 있는데요, add 하고 search 버튼이 있습니다. 이 둘 개 listener는 fragment으로부터 받아서 초기화가 될 겁니다.
onRightButtonClicked: 이건 샌드버드에서 제공해주는 함수로 원래 샌드버드 화면 기준으로 채팅방을 추가하는 버튼이 있는데 그 역할을 하는 함수입니다. 나중에 채팅방 추가하는 Activity까지 커스텀으로 만드면 제가 한 것처럼 직접 listener을 만들어서 주는 것이 좋습니다.
여기는 크게 하는 일 없이 단지 view를 inflate하고 click listener를 넣어주는 작업만 하고 있습니다.
다음은 ChannelList를 바꿔볼건데요, ChannelList은 RecyclerView를 이용해서 화면에 그리는데 여기서 사용하고 있는 adapter의 이름은 ChannelListAdapter입니다. 저희가 하는 스펙에서는 ChannelListComponent를 건들 필요 없이 adapter를 직접적으로 수정하면 됩니다.
class CustomChannelListAdapter : ChannelListAdapter() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): BaseViewHolder<GroupChannel> {
val binding =
CustomChannelItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ChannelListViewHolder(binding)
}
class ChannelListViewHolder(private val binding: CustomChannelItemBinding) :
BaseViewHolder<GroupChannel>(binding.root) {
override fun bind(item: GroupChannel) {
val context = itemView.context
val lastMessage = item.lastMessage
//set title
val members = item.members
var opponent: Member
if (members.size > 1) {
val currentUser = SendbirdChat.currentUser?.nickname ?: " "
opponent = if (members[0].nickname != currentUser) members[0] else members[1]
binding.tvName.text = opponent.nickname
} else {
val channel = item.name
binding.tvName.text = channel
}
// unread message count
val unreadCount = item.unreadMessageCount
binding.tvUnread.text =
if (unreadCount > 99) context.getString(com.sendbird.uikit.R.string.sb_text_channel_list_unread_count_max) else unreadCount.toString()
binding.tvUnread.visibility = if (unreadCount > 0) View.VISIBLE else View.GONE
binding.tvUnread.setBackgroundResource(R.drawable.circle_textview)
// channel cover image
Glide.with(binding.ivProfileImg.context).load(item.coverUrl).centerCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.placeholder)
.into(binding.ivProfileImg)
// last message whether user is typing or not
if (item.isTyping && item.typingUsers.isNotEmpty()) {
val typingUser = item.typingUsers
if (typingUser.size == 1) {
binding.tvLastMessage.text = String.format(
context.getString(com.sendbird.uikit.R.string.sb_text_channel_typing_indicator_single),
typingUser[0].nickname
)
}
} else {
if (lastMessage != null) {
binding.tvLastMessage.text = lastMessage.message
}
}
//last message time
item.lastMessage?.let {
val time = it.createdAt
val currentTime = System.currentTimeMillis()
val timeDif = kotlin.math.abs(currentTime - time)
val minutes = timeDif / (1000 * 60)
val hours = minutes / 60
val days = hours / 24
// Determine the appropriate time format to display.
val timeAgo = when {
days > 0 -> "$days days ago"
hours > 0 -> "$hours hours ago"
minutes > 0 -> "$minutes min ago"
else -> "Just now"
}
binding.tvLastMessageTime.text = timeAgo
}
}
}
}
하는 방법은 RecyclerView를 만드는것과 다를 게 없습니다. 단지 ViewHolder의 경우에는 기존에 쓰시던 RecyclerView.ViewHolder 대신해서 BaseViewHolder <GroupChannel>을 사용하셔야 합니다. 여기서 GroupChannel은 카톡의 개인톡과 단체톡으로 생각하시면 됩니다.
item: 샌드버드에서 제공하는 class로 채팅방에 대한 대부분의 정보를 가지고 있다고 생각하시면 됩니다. 맴버 숫자, 멤버 리스트, 마지막 메시지 시간, 만든 사람 등등 이런 정보들이 있어서 각각 viewHolder를 그릴 때 사용할 수 있습니다.
다음으로는 채팅방으로 보여줄께 없을 때 뜨는 에러 메시지를 수정해 보겠습니다.
class CustomStatus : StatusComponent() {
override fun onCreateView(
context: Context,
inflater: LayoutInflater,
parent: ViewGroup,
args: Bundle?,
): View {
params.errorText = "에러"
params.emptyText = "채팅방 없음"
params.emptyIcon = ContextCompat.getDrawable(context, R.drawable.multi)
return super.onCreateView(context, inflater, parent, args)
}
}
여기서는 크게할께 없습니다. StatusComponent는 params라고 하는 static class를 가지고 있는데 여기에 필요한 메시지나, Drawable을 관리하고 있어서 직접 수정하면 super.onCreateView에서 불러와 적용이 됩니다.
이제 adapter도 새로 만들었고, header도 새로 만들었고 에러 상황도 수정했는데, 이걸 적용하는 방식을 알려드리겠습니다
class CustomChannelList : ChannelListFragment() {
override fun onCreateModule(args: Bundle): ChannelListModule {
val module: ChannelListModule = super.onCreateModule(args)
module.setHeaderComponent(CustomHeader())
module.setStatusComponent(CustomStatus())
return module
}
override fun onBeforeReady(
status: ReadyStatus,
module: ChannelListModule,
viewModel: ChannelListViewModel,
) {
super.onBeforeReady(status, module, viewModel)
module.channelListComponent.setAdapter(CustomChannelListAdapter())
module.channelListComponent.setOnItemClickListener { _, _, channel ->
GroupChannel.getChannel(channel.url) { _, e ->
if (e != null) {
Toast.makeText(context, e.toString(), Toast.LENGTH_SHORT).show()
} else {
val intent = ChannelActivity.newIntent(
requireContext(),
channel.url
)
startActivity(intent)
}
}
}
}
override fun onBindHeaderComponent(
headerComponent: HeaderComponent,
viewModel: ChannelListViewModel,
) {
super.onBindHeaderComponent(headerComponent, viewModel)
if (headerComponent is CustomHeader) {
val header = module.headerComponent as CustomHeader
header.search = View.OnClickListener {
val intent = Intent(requireContext(), MessageSearchActivity::class.java)
startActivity(intent)
}
header.add = View.OnClickListener {
val intent = Intent(requireContext(), CreateChannelActivity::class.java)
startActivity(intent)
}
}
}
}
onCreateModule: 여기서 Module 즉 ChannelListModule을 설정해 줘야 하는데요, 여기서 Module에 있던 Component들을 세팅해 주면 됩니다. 저희는 HeaderComponent 하고 StatusComponent를 수정했으니 여기에 넣어주면 됩니다.
onBeforeReady: 여기서는 adapter를 ChannelListAdapter에 넣고 viewModel에 있는 Chat SDK를 통해 채팅방 리스트를 받아서 넣어주는 역할을 하고 있는데 저희들이 커스터마이즈한 adapter를 다시 추가해 주고 click listener까지 넣어서 채팅방으로 가는 intent를 받아서 시작해 주면 됩니다.
onBindHeaderComponent: viewholder에 있는 onBind()처럼 HeaderComponent에 있는 함수나, 변수에 접근을 할 수 있는데요. 여기서 HeaderComponent에 있는 search와 add click listener에 접근하여 각각 알맞는 activity를 실행시켜줬습니다.
headerComponent을 위해 있는 것처럼 onBindChannelListComponent, onBindStatusComponent도 있어서 필요하시다면 이들도 override 할 수 있습니다.
[마무리]
이번 글에서는 실제로 채팅방 리스트를 제가 원하는 형태로 바꿔봤는데요, 필요하시다면 더 많은 커스터마이즈를 하실 수도 있으십니다 조금만 예시를 들어보자면.
- Header에 더 많은 버튼을 넣기
- 밑에 따로 창을 넣기
- 리스트를 grid로 변경
- 채팅방이 한 개도 없으면 다른 화면으로 이동
기본적으로 제가 만든 코드 위에 추가되는 형식으로 진행될 겁니다.
전체적인 코드는 여기서 참고 부탁드립니다
https://github.com/flash159483/sendbird_customization/tree/main
'android > SendBird' 카테고리의 다른 글
(android/kotlin) 샌드버드 채팅(Channel) 커스텀 해보기 + 3.9 버전 변경 사항 (0) | 2023.09.28 |
---|---|
(Android, Kotlin) 샌드버드 UIKit과 Customization에 대해서 (0) | 2023.08.20 |