본문 바로가기
Android

Kotlin, FireBase 채팅어플 만들기 -8- [채팅창_activity]

by LasBe 2021. 9. 9.
반응형

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)

 

[참고]

하울의 코딩 채널 - YouTube

 

GitHub - LasBe-code/LasbeTalk

Contribute to LasBe-code/LasbeTalk development by creating an account on GitHub.

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

로 저장해야 배경으로써 이미지가 자동으로 텍스트에 맞게 조절된다.


[아쉬운 점]

-레이아웃의 비율

반응형

댓글


오픈 채팅