Kotlin, FireBase 채팅어플 만들기 -1- [Splash] (tistory.com)
Kotlin, FireBase 채팅어플 만들기 -2- [Login] (tistory.com)
Kotlin, FireBase 채팅어플 만들기 -3- [Sign Up] (tistory.com)
Kotlin, FireBase 채팅어플 만들기 -4- [Bottom Navigation] (tistory.com)
Kotlin, FireBase 채팅어플 만들기 -5- [친구창_fragment] (tistory.com)
Kotlin, FireBase 채팅어플 만들기 -6- [채팅 리스트_fragment] (tistory.com)
Kotlin, FireBase 채팅어플 만들기 -7- [프로필 변경_fragment] (tistory.com)
Kotlin, FireBase 채팅어플 만들기 -8- [채팅창_activity] (tistory.com)
[전체 코드 깃허브 주소]
LasBe-code/LasbeTalk (github.com)
[참고]
[XML]
<activity_message.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:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#FFAE37"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/messageActivity_textView_topName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginLeft="15dp"
android:layout_marginBottom="5dp"
android:fontFamily="sans-serif"
android:text="친구이름"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="60dp"
android:layout_marginBottom="55dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messageActivity_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/messageActivity_editText"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:layout_marginStart="5dp"
android:layout_marginLeft="5dp"
android:layout_marginEnd="55dp"
android:layout_marginRight="55dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/messageActivity_ImageView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/messageActivity_ImageView"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<item_message.xml>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/messageItem_linearlayout_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_marginTop="3dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/messageItem_layout_destination"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
<ImageView
android:id="@+id/messageItem_imageview_profile"
android:layout_width="50dp"
android:layout_height="50dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/messageItem_textview_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="이름"
android:textColor="#000000"
android:textStyle="bold" />
<TextView
android:id="@+id/messageItem_textView_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="메세지"
android:textColor="#000000" />
<TextView
android:id="@+id/messageItem_textView_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="시간"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
[데이터 모델]
package com.example.lasbetalk.model
data class Friend(
val email : String? = null,
val name : String? = null,
val profileImageUrl : String? = null,
val uid : String? = null)
///////////////////////////////////////////////
package com.example.lasbetalk.model
import kotlin.collections.HashMap
class ChatModel (val users: HashMap<String, Boolean> = HashMap(),
val comments : HashMap<String, Comment> = HashMap()){
class Comment(val uid: String? = null, val message: String? = null, val time: String? = null)
}
[액티비티 or 프래그먼트]
class MessageActivity : AppCompatActivity() {
private val fireDatabase = FirebaseDatabase.getInstance().reference
private var chatRoomUid : String? = null
private var destinationUid : String? = null
private var uid : String? = null
private var recyclerView : RecyclerView? = null
@SuppressLint("SimpleDateFormat")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_message)
val imageView = findViewById<ImageView>(R.id.messageActivity_ImageView)
val editText = findViewById<TextView>(R.id.messageActivity_editText)
//메세지를 보낸 시간
val time = System.currentTimeMillis()
val dateFormat = SimpleDateFormat("MM월dd일 hh:mm")
val curTime = dateFormat.format(Date(time)).toString()
destinationUid = intent.getStringExtra("destinationUid")
uid = Firebase.auth.currentUser?.uid.toString()
recyclerView = findViewById(R.id.messageActivity_recyclerview)
imageView.setOnClickListener {
Log.d("클릭 시 dest", "$destinationUid")
val chatModel = ChatModel()
chatModel.users.put(uid.toString(), true)
chatModel.users.put(destinationUid!!, true)
val comment = Comment(uid, editText.text.toString(), curTime)
if(chatRoomUid == null){
imageView.isEnabled = false
fireDatabase.child("chatrooms").push().setValue(chatModel).addOnSuccessListener {
//채팅방 생성
checkChatRoom()
//메세지 보내기
Handler().postDelayed({
println(chatRoomUid)
fireDatabase.child("chatrooms").child(chatRoomUid.toString()).child("comments").push().setValue(comment)
messageActivity_editText.text = null
}, 1000L)
Log.d("chatUidNull dest", "$destinationUid")
}
}else{
fireDatabase.child("chatrooms").child(chatRoomUid.toString()).child("comments").push().setValue(comment)
messageActivity_editText.text = null
Log.d("chatUidNotNull dest", "$destinationUid")
}
}
checkChatRoom()
}
private fun checkChatRoom(){
fireDatabase.child("chatrooms").orderByChild("users/$uid").equalTo(true)
.addListenerForSingleValueEvent(object : ValueEventListener{
override fun onCancelled(error: DatabaseError) {
}
override fun onDataChange(snapshot: DataSnapshot) {
for (item in snapshot.children){
println(item)
val chatModel = item.getValue<ChatModel>()
if(chatModel?.users!!.containsKey(destinationUid)){
chatRoomUid = item.key
messageActivity_ImageView.isEnabled = true
recyclerView?.layoutManager = LinearLayoutManager(this@MessageActivity)
recyclerView?.adapter = RecyclerViewAdapter()
}
}
}
})
}
inner class RecyclerViewAdapter : RecyclerView.Adapter<RecyclerViewAdapter.MessageViewHolder>() {
private val comments = ArrayList<Comment>()
private var friend : Friend? = null
init{
fireDatabase.child("users").child(destinationUid.toString()).addListenerForSingleValueEvent(object : ValueEventListener{
override fun onCancelled(error: DatabaseError) {
}
override fun onDataChange(snapshot: DataSnapshot) {
friend = snapshot.getValue<Friend>()
messageActivity_textView_topName.text = friend?.name
getMessageList()
}
})
}
fun getMessageList(){
fireDatabase.child("chatrooms").child(chatRoomUid.toString()).child("comments").addValueEventListener(object : ValueEventListener{
override fun onCancelled(error: DatabaseError) {
}
override fun onDataChange(snapshot: DataSnapshot) {
comments.clear()
for(data in snapshot.children){
val item = data.getValue<Comment>()
comments.add(item!!)
println(comments)
}
notifyDataSetChanged()
//메세지를 보낼 시 화면을 맨 밑으로 내림
recyclerView?.scrollToPosition(comments.size - 1)
}
})
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
val view : View = LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false)
return MessageViewHolder(view)
}
@SuppressLint("RtlHardcoded")
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
holder.textView_message.textSize = 20F
holder.textView_message.text = comments[position].message
holder.textView_time.text = comments[position].time
if(comments[position].uid.equals(uid)){ // 본인 채팅
holder.textView_message.setBackgroundResource(R.drawable.rightbubble)
holder.textView_name.visibility = View.INVISIBLE
holder.layout_destination.visibility = View.INVISIBLE
holder.layout_main.gravity = Gravity.RIGHT
}else{ // 상대방 채팅
Glide.with(holder.itemView.context)
.load(friend?.profileImageUrl)
.apply(RequestOptions().circleCrop())
.into(holder.imageView_profile)
holder.textView_name.text = friend?.name
holder.layout_destination.visibility = View.VISIBLE
holder.textView_name.visibility = View.VISIBLE
holder.textView_message.setBackgroundResource(R.drawable.leftbubble)
holder.layout_main.gravity = Gravity.LEFT
}
}
inner class MessageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView_message: TextView = view.findViewById(R.id.messageItem_textView_message)
val textView_name: TextView = view.findViewById(R.id.messageItem_textview_name)
val imageView_profile: ImageView = view.findViewById(R.id.messageItem_imageview_profile)
val layout_destination: LinearLayout = view.findViewById(R.id.messageItem_layout_destination)
val layout_main: LinearLayout = view.findViewById(R.id.messageItem_linearlayout_main)
val textView_time : TextView = view.findViewById(R.id.messageItem_textView_time)
}
override fun getItemCount(): Int {
return comments.size
}
}
}
드디어 제일 중요하고 어려웠던 채팅창이다.
실행되면 바로 checkChatRoom()을 통해 현재 접속한 채팅방을 검사해준다.
데이터베이스의 chatrooms에서 자신의 uid가 포함된 채팅방들을 가져온 후
기존에 만들었던 채팅방이 있다면
getStringExtra로 가져온 상대방의 uid가 포함된 chatroom의 uid를 chatRoomUid에 담아주고
기존의 채팅들을 리사이클러뷰로 뿌려준다.
private fun checkChatRoom(){
fireDatabase.child("chatrooms").orderByChild("users/$uid").equalTo(true)
.addListenerForSingleValueEvent(object : ValueEventListener{
override fun onCancelled(error: DatabaseError) {
}
override fun onDataChange(snapshot: DataSnapshot) {
for (item in snapshot.children){
println(item)
val chatModel = item.getValue<ChatModel>()
if(chatModel?.users!!.containsKey(destinationUid)){
chatRoomUid = item.key
messageActivity_ImageView.isEnabled = true
recyclerView?.layoutManager = LinearLayoutManager(this@MessageActivity)
recyclerView?.adapter = RecyclerViewAdapter()
}
}
}
})
}
리사이클러뷰 어댑터는 초기화 하면서 상대방의 정보를 받아오고
상단바에 친구의 이름을 띄어준다.
그 다음 getMessageList()를 실행한다.
init{
fireDatabase.child("users").child(destinationUid.toString()).addListenerForSingleValueEvent(object : ValueEventListener{
override fun onCancelled(error: DatabaseError) {
}
override fun onDataChange(snapshot: DataSnapshot) {
friend = snapshot.getValue<Friend>()
messageActivity_textView_topName.text = friend?.name
getMessageList()
}
})
}
서로가 보낸 메세지를 comments에 받아온다.
scrollToPosition(commnets.size - 1)을 통해 메세지를 전송 시 최근에 보낸 comments의 Position으로 화면을 이동시킨다.
fun getMessageList(){
fireDatabase.child("chatrooms").child(chatRoomUid.toString()).child("comments").addValueEventListener(object : ValueEventListener{
override fun onCancelled(error: DatabaseError) {
}
override fun onDataChange(snapshot: DataSnapshot) {
comments.clear()
for(data in snapshot.children){
val item = data.getValue<Comment>()
comments.add(item!!)
println(comments)
}
notifyDataSetChanged()
//메세지를 보낼 시 화면을 맨 밑으로 내림
recyclerView?.scrollToPosition(comments.size - 1)
}
})
}
이제 메세지 전송 이미지뷰를 클릭했을 때
자신의 uid, 보낼 텍스트, 현재 시간을 comment에 담는다.
만약 처음 checkChatRoom을 통해 데이터베이스에 만들어진 채팅방이 없는 것을 확인 했을 때
push를 통해 채팅방을 생성해준다.
Handler를 통해 checkChatRoom()이 종료하고 1초 뒤에 메세지를 push해준다.
딜레이를 준 것은 checkChatRoom()으로 새로운 채팅방에 대한 정보를 받아오기 전에
데이터베이스에 chatRoomUid가 null인 채로 메세지를 push하기 때문이다.
chatCheckRoom 함수에 대한 종료 콜백을 받아 메세지를 푸시하면 낭비가 없을 것 같은데 아직은 방법을 모르겠다.
데이터베이스에 기존의 채팅방이 있을 경우 메세지를 그대로 푸시해준다.
imageView.setOnClickListener {
Log.d("클릭 시 dest", "$destinationUid")
val chatModel = ChatModel()
chatModel.users.put(uid.toString(), true)
chatModel.users.put(destinationUid!!, true)
val comment = Comment(uid, editText.text.toString(), curTime)
if(chatRoomUid == null){
imageView.isEnabled = false
fireDatabase.child("chatrooms").push().setValue(chatModel).addOnSuccessListener {
//채팅방 생성
checkChatRoom()
//메세지 보내기
Handler().postDelayed({
println(chatRoomUid)
fireDatabase.child("chatrooms").child(chatRoomUid.toString()).child("comments").push().setValue(comment)
messageActivity_editText.text = null
}, 1000L)
Log.d("chatUidNull dest", "$destinationUid")
}
}else{
fireDatabase.child("chatrooms").child(chatRoomUid.toString()).child("comments").push().setValue(comment)
messageActivity_editText.text = null
Log.d("chatUidNotNull dest", "$destinationUid")
}
}
어댑터 내의 onBindViewHolder에서는 comments의 uid로 자신과 상대를 구분해 채팅을 띄어준다.
자신이 보낸 메세지일 경우 텍스트만 띄워주고
INVISIBLE을 통해 필요없는 것들을 숨김처리하며
layout의 gravity는 오른쪽으로 설정한다.
상대방이 보낸 채팅일 경우
glide를 통해 프로필 사진을 뿌려주고
이름, 텍스트, 시간을 띄워준다.
물론 gravity는 왼쪽으로 설정한다.
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
holder.textView_message.textSize = 20F
holder.textView_message.text = comments[position].message
holder.textView_time.text = comments[position].time
if(comments[position].uid.equals(uid)){ // 본인 채팅
holder.textView_message.setBackgroundResource(R.drawable.rightbubble)
holder.textView_name.visibility = View.INVISIBLE
holder.layout_destination.visibility = View.INVISIBLE
holder.layout_main.gravity = Gravity.RIGHT
}else{ // 상대방 채팅
Glide.with(holder.itemView.context)
.load(friend?.profileImageUrl)
.apply(RequestOptions().circleCrop())
.into(holder.imageView_profile)
holder.textView_name.text = friend?.name
holder.layout_destination.visibility = View.VISIBLE
holder.textView_name.visibility = View.VISIBLE
holder.textView_message.setBackgroundResource(R.drawable.leftbubble)
holder.layout_main.gravity = Gravity.LEFT
}
}
[작동]
[Tip]
말풍선.png 파일을 저장할 때 확장자 명을 꼭꼭
말풍선.9.png
로 저장해야 배경으로써 이미지가 자동으로 텍스트에 맞게 조절된다.
[아쉬운 점]
-레이아웃의 비율
'Android' 카테고리의 다른 글
Kotlin, FireBase 채팅어플 만들기 -7- [프로필 변경_fragment] (2) | 2021.09.09 |
---|---|
Kotlin, FireBase 채팅어플 만들기 -6- [채팅 리스트_fragment] (0) | 2021.09.09 |
Kotlin, FireBase 채팅어플 만들기 -5- [친구창_fragment] (0) | 2021.09.07 |
Kotlin, FireBase 채팅어플 만들기 -4- [Bottom Navigation] (0) | 2021.09.04 |
Kotlin, FireBase 채팅어플 만들기 -3- [Sign Up] (0) | 2021.09.02 |
댓글