채팅방도 생성했고, 채팅방에 유저도 초대했으니, 이제 마지막으로 메시지를 주고 받을 수 있는 기능을 추가해보도록 하겠습니다.
① FirebaseRef에 아래의 내용을 입력한다.
val message = database.getReference("message")
② 메시지 전송에 사용될 data class를 정의하자. chat 디렉토리 하위로 MessageModel이라는 이름의 kotlin class를 추가한다.
data class MessageModel (
val senderUid : String = "",
val senderNickName : String = "",
val senderProfileUrl : String = "",
val contents : String = "",
val sendTime : String = "",
var viewType: Int = VIEW_TYPE_ME
) {
companion object {
const val VIEW_TYPE_YOU = 0
const val VIEW_TYPE_ME = 1
}
}
③ ChatRoomActivity에 send 버튼에 대한 클릭 이벤트 리스너를 등록하자.
class ChatRoomActivity : AppCompatActivity() {
lateinit var count : String
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_room)
val roomName = findViewById<TextView>(R.id.chatRoomName)
val nickNameList = findViewById<TextView>(R.id.nickNameList)
val inviteBtn = findViewById<Button>(R.id.invite)
val userCount = findViewById<TextView>(R.id.userCount)
// Intent로부터 데이터를 가져옴
val chatRoomName = intent.getStringExtra("chatRoomName")
roomName.text = chatRoomName
val roomId = intent.getStringExtra("chatRoomId")
val chatRoomId = roomId
val userList = intent.getStringExtra("userList")
nickNameList.text = userList
CoroutineScope(Dispatchers.IO).launch {
val response = getUserCount(chatRoomId!!)
Log.d("userCount", response.toString())
if (response.isSuccess) {
count = response.result.toString()
userCount.text = count
} else {
Log.d("UserListFragment", "유저의 정보를 불러오지 못함")
}
}
inviteBtn.setOnClickListener {
val intent = Intent(this, InviteActivity::class.java)
intent.putExtra("chatRoomId", chatRoomId)
intent.putExtra("chatRoomName", chatRoomName)
intent.putExtra("nickNameList", userList)
startActivity(intent)
}
val participantBtn = findViewById<ImageView>(R.id.participants)
participantBtn.setOnClickListener {
val intent = Intent(this, ParticipantsActivity::class.java)
intent.putExtra("chatRoomId", chatRoomId)
startActivity(intent)
}
val message = findViewById<TextInputEditText>(R.id.message)
val sendBtn = findViewById<ImageView>(R.id.send)
lateinit var myNickName : String
lateinit var myProfileUrl : String
lateinit var messageModel: MessageModel
val myUid = FirebaseAuthUtils.getUid()
sendBtn.setOnClickListener {
val contents = message.text.toString()
val sendTime = getSendTime()
CoroutineScope(Dispatchers.IO).launch {
val response = getUserInfo(myUid)
if (response.isSuccess) {
myNickName = response.result?.nickName.toString()
myProfileUrl = response.result?.imgUrl.toString()
messageModel = MessageModel(myUid, myNickName, myProfileUrl, contents, sendTime)
FirebaseRef.message.child(chatRoomId!!).push().setValue(messageModel)
} else {
Log.d("UserListFragment", "유저의 정보를 불러오지 못함")
}
}
message.text?.clear()
}
}
private suspend fun getUserCount(roomId: String) : BaseResponse<String> {
return RetrofitInstance.chatApi.getUserCount(roomId)
}
private suspend fun getUserInfo(uid: String): BaseResponse<GetUserRes> {
return RetrofitInstance.myPageApi.getUserInfo(uid)
}
//메시지 보낸 시각 정보 반환
@RequiresApi(Build.VERSION_CODES.O)
private fun getSendTime(): String {
try {
val localDateTime = LocalDateTime.now()
val dateTimeFormatter = DateTimeFormatter.ofPattern("a h:mm")
return localDateTime.format(dateTimeFormatter)
} catch (e: Exception) {
e.printStackTrace()
throw Exception("시간 정보를 불러오지 못함")
}
}
}
④ drawable 디렉토리 하위로 message_box라는 이름의 리소스 파일을 추가한다.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/skyBlue"/>
<corners android:radius="15dp"/>
</shape>
⑤ 메시지 목록을 Recycler View로 보여주기 위해 layout 디렉토리 하위로, message_recycler_view_item이라는 이름의 리소스 파일을 추가하자.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/messageContentsArea"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:layout_marginEnd="8dp"
android:background="@drawable/message_box"
android:ellipsize="end"
android:maxWidth="230dp"
android:padding="10dp"
android:text="my text message"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@+id/messageProfileArea"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/dateTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginEnd="15dp"
android:text="9/7 7:01 AM"
android:textSize="10sp"
app:layout_constraintEnd_toStartOf="@+id/messageProfileArea"
app:layout_constraintTop_toBottomOf="@+id/messageContentsArea" />
<ImageView
android:id="@+id/messageProfileArea"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="15dp"
android:layout_marginRight="15dp"
android:src="@drawable/profile"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/messageNickName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="17dp"
android:layout_marginBottom="2dp"
android:gravity="center"
android:text="nickname"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/messageContentsArea"
app:layout_constraintEnd_toStartOf="@+id/messageProfileArea" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
⑥ 다른 사람의 메시지 박스에 적용할 your_message_box라는 이름의 리소스 파일을 추가한다.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/skyBlue"/>
<corners android:radius="15dp"/>
</shape>
</item>
<item android:right="3dp" android:left="3dp" android:top="3dp" android:bottom="3dp">
<shape android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="14dp"/>
</shape>
</item>
</layer-list>
⑦ 나의 채팅을 오른쪽에, 상대방의 채팅을 왼쪽에 표시하기 위해서 layout 디렉토리 하위로, message_recycler_view_item2를 추가한다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/messageContentsArea2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="35dp"
android:background="@drawable/your_message_box"
android:ellipsize="end"
android:maxWidth="230dp"
android:padding="10dp"
android:text="my text message"
android:textSize="15sp"
app:layout_constraintStart_toEndOf="@+id/messageProfileArea2"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/dateTime2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="9/7 7:01 AM"
android:textSize="10sp"
android:layout_marginTop="5dp"
android:layout_marginStart="15dp"
app:layout_constraintStart_toEndOf="@+id/messageProfileArea2"
app:layout_constraintTop_toBottomOf="@+id/messageContentsArea2" />
<ImageView
android:id="@+id/messageProfileArea2"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="15dp"
android:layout_marginLeft="15dp"
android:src="@drawable/profile"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/messageNickName2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="17dp"
android:gravity="center"
android:text="nickname"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/messageContentsArea2"
app:layout_constraintStart_toEndOf="@+id/messageProfileArea2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
messageModel = MessageModel(myUid, myNickName, myProfileUrl, contents, sendTime, 1)
⑨ activity_chat_room.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:background="@drawable/main_border"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".chat.ChatRoomActivity">
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="145dp"
android:background="@drawable/main_border"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/userCount"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="55dp"
android:layout_marginLeft="10dp"
android:text="1"
android:gravity="right"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@+id/roomName"
app:layout_constraintHorizontal_bias="0.774"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="35dp"
android:layout_height="30dp"
android:layout_marginTop="55dp"
android:gravity="right"
android:text="명 :"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@+id/roomName"
app:layout_constraintHorizontal_bias="0.774"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/nickNameList"
android:layout_width="170dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="20dp"
android:layout_marginTop="55dp"
android:maxWidth="170dp"
android:text="User List"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@+id/roomName"
app:layout_constraintHorizontal_bias="0.774"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/invite"
android:layout_width="75dp"
android:layout_height="40dp"
android:layout_marginTop="90dp"
android:background="@color/skyBlue"
android:text="초대"
android:textSize="20sp" />
</LinearLayout>
<ImageView
android:id="@+id/participants"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="10dp"
android:layout_marginRight="15dp"
android:src="@drawable/participants"
android:textSize="25sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/chatRoomName"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="ChatRoomName"
android:textColor="#000000"
android:textSize="30sp"
android:layout_marginTop="10dp"
android:layout_marginHorizontal="20dp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
tools:layout_editor_absoluteX="-19dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/message"
android:layout_width="280dp"
android:layout_height="50dp"
android:layout_marginLeft="13dp"
android:layout_marginBottom="10dp"
android:background="@drawable/main_border"
android:hint="텍스트를 입력하세요"
android:padding="5dp"
android:textColorHint="#808080"
android:textSize="25sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/send"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:src="@drawable/send" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/linearLayout3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout2"
app:layout_constraintVertical_bias="1.0"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messageRV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/linearLayout3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout2"
app:layout_constraintVertical_bias="1.0"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
⑩ chat 패키지 하위로 MessageAdapter 클래스를 추가한다.
class MessageAdapter(private val context: Context, val items: MutableList<MessageModel>)
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class ItemViewHolder1(private val view: View) : RecyclerView.ViewHolder(view) {
val contents : TextView = view.findViewById(R.id.messageContentsArea)
val dateTime : TextView = view.findViewById(R.id.dateTime)
val nickName : TextView = view.findViewById(R.id.messageNickName)
val profile : ImageView = view.findViewById(R.id.messageProfileArea)
}
class ItemViewHolder2(private val view: View) : RecyclerView.ViewHolder(view) {
val contents : TextView = view.findViewById(R.id.messageContentsArea2)
val dateTime : TextView = view.findViewById(R.id.dateTime2)
val nickName : TextView = view.findViewById(R.id.messageNickName2)
val profile : ImageView = view.findViewById(R.id.messageProfileArea2)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val adapterLayout : View?
return when(viewType) {
MessageModel.VIEW_TYPE_ME -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.message_recycler_view_item, parent, false)
ItemViewHolder1(adapterLayout)
}
MessageModel.VIEW_TYPE_YOU -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.message_recycler_view_item2, parent, false)
ItemViewHolder2(adapterLayout)
}
else -> throw RuntimeException("Invalid View Type Error")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
when (item.viewType) {
MessageModel.VIEW_TYPE_ME -> {
(holder as ItemViewHolder1).contents.text = item.contents
holder.dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
}
MessageModel.VIEW_TYPE_YOU -> {
(holder as ItemViewHolder2).contents.text = item.contents
holder.dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
}
}
}
override fun getItemCount(): Int {
return items.size
}
override fun getItemViewType(position: Int): Int {
return items[position].viewType
}
}
⑪ ChatRoomActivity를 아래와 같이 수정한다.
class ChatRoomActivity : AppCompatActivity() {
lateinit var count : String
lateinit var messageAdapter : MessageAdapter
val messageList = mutableListOf<MessageModel>()
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_room)
val roomName = findViewById<TextView>(R.id.chatRoomName)
val nickNameList = findViewById<TextView>(R.id.nickNameList)
val inviteBtn = findViewById<Button>(R.id.invite)
val userCount = findViewById<TextView>(R.id.userCount)
// Intent로부터 데이터를 가져옴
val chatRoomName = intent.getStringExtra("chatRoomName")
roomName.text = chatRoomName
val roomId = intent.getStringExtra("chatRoomId")
val chatRoomId = roomId
val userList = intent.getStringExtra("userList")
nickNameList.text = userList
CoroutineScope(Dispatchers.IO).launch {
val response = getUserCount(chatRoomId!!)
Log.d("userCount", response.toString())
if (response.isSuccess) {
count = response.result.toString()
userCount.text = count
} else {
Log.d("UserListFragment", "유저의 정보를 불러오지 못함")
}
}
val recyclerView = findViewById<RecyclerView>(R.id.messageRV)
messageAdapter = MessageAdapter(this, messageList)
recyclerView.adapter = messageAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
getMessageList(chatRoomId!!)
Log.d("MessageList", messageList.toString())
inviteBtn.setOnClickListener {
val intent = Intent(this, InviteActivity::class.java)
intent.putExtra("chatRoomId", chatRoomId)
intent.putExtra("chatRoomName", chatRoomName)
intent.putExtra("nickNameList", userList)
startActivity(intent)
}
val participantBtn = findViewById<ImageView>(R.id.participants)
participantBtn.setOnClickListener {
val intent = Intent(this, ParticipantsActivity::class.java)
intent.putExtra("chatRoomId", chatRoomId)
startActivity(intent)
}
val message = findViewById<TextInputEditText>(R.id.message)
val sendBtn = findViewById<ImageView>(R.id.send)
lateinit var myNickName : String
lateinit var myProfileUrl : String
lateinit var messageModel: MessageModel
val myUid = FirebaseAuthUtils.getUid()
sendBtn.setOnClickListener {
val contents = message.text.toString()
val sendTime = getSendTime()
CoroutineScope(Dispatchers.IO).launch {
val response = getUserInfo(myUid)
if (response.isSuccess) {
myNickName = response.result?.nickName.toString()
myProfileUrl = response.result?.imgUrl.toString()
messageModel = MessageModel(myUid, myNickName, myProfileUrl, contents, sendTime)
FirebaseRef.message.child(chatRoomId!!).push().setValue(messageModel)
} else {
Log.d("ChatRoomActivity", "유저의 정보를 불러오지 못함")
}
}
message.text?.clear()
}
}
private suspend fun getUserCount(roomId: String) : BaseResponse<String> {
return RetrofitInstance.chatApi.getUserCount(roomId)
}
private suspend fun getUserInfo(uid: String): BaseResponse<GetUserRes> {
return RetrofitInstance.myPageApi.getUserInfo(uid)
}
//메시지 보낸 시각 정보 반환
@RequiresApi(Build.VERSION_CODES.O)
private fun getSendTime(): String {
try {
val localDateTime = LocalDateTime.now()
val dateTimeFormatter = DateTimeFormatter.ofPattern("M/d h:mm a")
return localDateTime.format(dateTimeFormatter)
} catch (e: Exception) {
e.printStackTrace()
throw Exception("시간 정보를 불러오지 못함")
}
}
private fun getMessageList(chatRoomId : String) {
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
messageList.clear()
val newMessages = mutableListOf<MessageModel>() // 새로운 메시지를 저장할 리스트 생성
for (datamModel in dataSnapshot.children) {
val messageModel = datamModel.getValue(MessageModel::class.java)
if(messageModel!!.senderUid != FirebaseAuthUtils.getUid()) {
messageModel.viewType = MessageModel.VIEW_TYPE_YOU
}
newMessages.add(messageModel!!)
}
messageList.addAll(newMessages)
messageAdapter.notifyDataSetChanged()
Log.d("MessageList", messageList.toString())
}
override fun onCancelled(databaseError: DatabaseError) {
Log.w("MyMessage", "onCancelled", databaseError.toException())
}
}
FirebaseRef.message.child(chatRoomId).addValueEventListener(postListener)
}
}
이제 코드를 실행시켜보자. AVD 또는 실제 본인의 휴대폰을 이용해 두 개 이상의 디바이스에서 테스트해보기 바란다. (만약 본인의 휴대폰으로 테스트할거라면, 먼저 서버에 배포해야 한다. 서버에 배포하는 방법을 모르거나 귀찮다면 AVD 두 개로 테스트해야 한다.) 실시간으로 채팅 메시지가 업데이트 되어야하고, 상대방의 메시지와 나의 메시지가 다른 레이아웃으로 구성되어야 한다.
지금의 코드는 새로운 메시지를 보내거나 받아도 포커스가 이동하지 않는다. 어플리케이션의 동작에는 전혀 무리가 없으나, UX 측면에서의 불편함이 있을 수는 있다. 그러한 경우 아래와 같이 ChatRoomAcitvity의 getMessageList 메서드를 수정하면 된다.
class ChatRoomActivity : AppCompatActivity() {
lateinit var recyclerView : RecyclerView // 리사이클러뷰를 lateinit으로 선언
...
private fun getMessageList(chatRoomId : String) {
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
...
messageList.addAll(newMessages)
messageAdapter.notifyDataSetChanged()
Log.d("MessageList", messageList.toString())
recyclerView.post {
recyclerView.scrollToPosition(recyclerView.adapter?.itemCount?.minus(1) ?: 0)
}
}
이제 새로운 메시지로 포커스가 이동하는 것을 확인할 수 있을 것이다. 또한 채팅방에 나갔다가 다시 들어갈 때에도 가장 최근 메시지에 포커싱된다.