[Android/Compose] Firebase Realtime DB로 채팅 구현

찌니·2022년 8월 31일
0

Android(Compose)

목록 보기
9/12
post-thumbnail

(이 코드는 게시글 작성자에게 채팅을 거는 방식으로, writer와 contact 존재)
1. 유저들은 고유 userId 존재
2. Userdata에는 상대방의 아이디와 함께 닉네임 존재
4. 채팅아이디는 (게시글 아이디 + contact유저 id)의 구조
5. 채팅은 리스트로 받음
6. 메세지는 [보낸 시간, 메세지 내용, 보낸 userId]의 구조
7. 게시글에서 채팅을 건 시점의 코드
8. 유저별로 채팅id 목록 리스트 존재

ChatData.kt - DataClass

data class ChatData(
    val id: String?,
    val writer: UserData,
    val contact: UserData,
    val messages: List<MessageData>
)
data class UserData(
    val id: Int?,
    val nickname: String
)
data class MessageData(
    val content: String,
    val createdAt: Long,
    val from: Int
)

ChatViewModel.kt

    var chatMessages = MutableStateFlow<List<MessageData>>(emptyList())
    var chatData = MutableStateFlow<ChatData?>(null)
    var userId = 29 (임시)

    // 채팅방 들어가자마자 조회 - 한번도 채팅하지 않은 경우(채팅방이 생성되어있지 않은 경우) 조회불가
    fun enterChatRoom(chatId: String){
        // 한번도 채팅하지 않은경우는 조회 불가
        Log.d("채팅방 id", chatId)
        firebaseDB.reference.child("chat").child(chatId).get()
            .addOnSuccessListener {
                Log.d("채팅방 정보", it.value.toString())
                // 데이터는 hashMap 형태로 오기때문에 객체 형태로 변환해줘야함
                it.value?.let { value ->
                    val result = value as HashMap<String, Any>?
                    val writer = result?.get("writer") as HashMap<String, Any>?
                    val contact = result?.get("contact") as HashMap<String, Any>?
                    val _chatData = ChatData(
                        result?.get("id") as String,
                        UserData((writer?.get("id") as Long).toInt(), writer["nickname"] as String, (writer["level"] as Long).toInt()),
                        UserData((contact?.get("id") as Long).toInt(), contact["nickname"] as String, (contact["level"] as Long).toInt()),
                        result["messages"] as List<MessageData>
                    )
                    chatData.value = _chatData
                }
            }
            .addOnFailureListener{
                Log.d("채팅룸 정보 가져오기 실패", it.toString())
            }

        val chatListener = object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                val _chatMessage = arrayListOf<MessageData>()
                val messageData = snapshot.value as ArrayList<HashMap<String, Any>>?

                // snapshot은 hashMap 형태로 오기때문에 객체 형태로 변환해줘야함
                messageData?.forEach {
                    _chatMessage.add(
                        MessageData(
                            it["content"] as String,
                            it["createdAt"] as Long,
                            (it["from"] as Long).toInt()
                        )
                    )
                }
                chatMessages.value = arrayListOf()
                chatMessages.value = _chatMessage.toList()
                Log.d("변화 리스너2", chatMessages.value.toString())
            }

            override fun onCancelled(error: DatabaseError) {
                Log.d(TAG, "loadMessage:onCancelled", error.toException())
            }
        }
        firebaseDB.reference.child("chat").child(chatId).child("messages").addValueEventListener(chatListener)
        Firebase
    }
    
    // 메세지 보내기
    fun newMessage(chatId: String, messageData: MessageData){
        if(chatMessages.value.isEmpty()) {
        	chatMessages.value = listOf(messageData)
            // 첫 메세지일때 채팅방 생성 - 채팅룸이 생성되는 시점
            newChatRoom(chatId, userId, chatMessages.value)
        }else{
            chatMessages.value += messageData
            firebaseDB.reference.child("chat").child(chatId).child("messages").setValue(chatMessages.value)
                .addOnSuccessListener {
                    Log.d("newChatRoomSuccess", "메세지 보내기 성공")
                }
                .addOnFailureListener{
                    Log.d("메세지 보내기 실패", it.toString())
                }
        }
    }

    // 채팅룸 생성
    private fun newChatRoom(chatId: String, postId: Int, writerId: Int, message :List<MessageData>){
        val userId = UserSharedPreference(App.context()).getUserPrefs("id").toString()
 
        var usersChatList: List<String> = emptyList()

        viewModelScope.launch {
            viewModelScope.async {
                firebaseDB.reference.child("user").child(userId).get()
                    .addOnSuccessListener {
                        it.value?.let { it ->
                            usersChatList = it as List<String>
                        }
                        Log.d("유저 채팅 목록 가져옴", usersChatList.toString())

                    }
                    .addOnFailureListener {
                        isHave = false
                    }
            }.await()
             // db에서 user정보 가져옴
            val writerData: ArrayList<UserData> = dbAccessModule.getUserInfoById(writerId)
                val writer = UserData(writerData[0].id, writerData[0].nickname)

                // sharedPreference에 저장된 본인 user정보
                val contactData = UserSharedPreference(App.context()).getUserPrefs()
                val contact = RdbUserData(contactData.id, contactData.nickname

                val chatData = ChatData(chatId, writer, contact, message)

                firebaseDB.reference.child("chat").child(chatId).setValue(chatData)
                    .addOnSuccessListener {
                        Log.d("newChatRoomSuccess", "채팅룸 생성 완료")
                        // 생성시 채팅 listener 재호출
                        enterChatRoom(chatId)
                    }
                    .addOnFailureListener {
                        Log.d("채팅룸 생성 실패", it.toString())
                    }

                if (usersChatList.isEmpty()){
                    usersChatList = listOf(chatId)
                }else {
                    usersChatList.plus(chatId)
                }
                Log.d("유저 채팅 리스트", usersChatList.toString())

                firebaseDB.reference.child("user").child(userId).setValue(usersChatList)
                    .addOnSuccessListener {
                    Log.d("newChatRoomSuccess", "유저 정보에 추가 완료")
                    enterChatRoom(chatId)
                }
                    .addOnFailureListener {
                        Log.d("유저 정보 추가 실패", it.toString())
                  }
          }

Chat.kt - UI


    val scaffoldState = rememberScaffoldState()

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = { Text(text = "", textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), fontSize = 17.sp) },
                navigationIcon = {
                    IconButton(onClick = { navController.navigateUp() }) {
                        Icon(painterResource(id = R.drawable.icon_back), contentDescription = "뒤로가기", modifier = Modifier.size(35.dp), tint = colorResource(
                            id = R.color.green)
                        )
                    }
                },
                actions = {
                    IconButton(onClick = { declarationDialogState = true }) {
                        Icon(painterResource(id = R.drawable.icon_decl), contentDescription = "신고하기", modifier = Modifier.size(45.dp), tint = Color.Red)
                    }
                },
                backgroundColor = Color.Transparent,
                elevation = 0.dp
            )
        }
    ) { it ->
        Column(modifier = Modifier.padding(it), verticalArrangement = Arrangement.Bottom) {
             ChatSection(chatViewModel.chatMessages.collectAsState(),chatViewModel.chatData.collectAsState(), userId, Modifier.weight(1f))
                SendSection(chatViewModel, chatId, userId)
            }
        }
        
        LaunchedEffect(Unit) {
        // 채팅창 들어가서 정보 가져오기- 채팅 데이터, 채팅 내용
        chatViewModel.enterChatRoom(chatId)
    }

}

@Composable
fun ChatSection(message: State<List<MessageData>?>, chatData: State<ChatData?>, userId: Int, modifier: Modifier = Modifier) {
    //userId = 내 아이디
    val writerId: Int? = chatData.value?.writer?.id

    // 상대방 닉네임
    val nickname = if(writerId == userId){
        chatData.value?.contact?.nickname
    }else{
        chatData.value?.writer?.nickname
    }


    LazyColumn(
        modifier = Modifier
            .padding(start = 15.dp, end = 15.dp, top = 15.dp),
        verticalArrangement = Arrangement.Bottom
    ) {
        message.value?.let {
            items(it) { message ->
                nickname?.let { nickname ->
                    MessageItem(message, userId, nickname)
                    Spacer(modifier = Modifier.height(13.dp))
                }
            }
        }
    }
}

@Composable
fun MessageItem(message: MessageData, userId: Int, nickname: String) {
    val current = System.currentTimeMillis()

    val calculateTime = CalculateTime()
    // 시간 계산 -  방금, 몇분 전, 몇시 몇분
    val time = calculateTime.calTimeToChat(current, message.createdAt)


    // 본인일때 true
    val isMe = message.from == userId
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = if (message.from != userId) Arrangement.Start else Arrangement.End,
        verticalAlignment = Alignment.Bottom
    ) {
        if (isMe) {
            Text(text = time, color = Color.Gray, modifier = Modifier.padding(start = 7.dp, end = 7.dp), fontSize = 13.sp)
        }
        Column {
            if (!isMe) {
                Row{
                    Text(text = nickname, color = Color.Black, fontSize = 13.sp, modifier = Modifier.padding(bottom = 5.dp, end = 5.dp))
                    Image(
                        painter = painterResource(R.drawable.sprout),
                        contentDescription = "App icon",
                        modifier = Modifier
                            .clip(shape = CircleShape)
                            .size(17.dp)
                    )
                }
            }
            if (message.content != "") {
                Box(
                    modifier = if (isMe) {
                        Modifier
                            .background(
                                color = Color.Yellow,
                                shape = RoundedCornerShape(10.dp, 0.dp, 10.dp, 10.dp)
                            )
                            .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 16.dp)
                    } else {
                        Modifier
                            .background(
                                color = Color.LightGray,
                                shape = RoundedCornerShape(0.dp, 10.dp, 10.dp, 10.dp)
                            )
                            .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 16.dp)
                    }
                ) {
                    Text(text = message.content, color = Color.Black)
                }
            }
        }
        if (!isMe) {
            Text(text = time, color = Color.Gray, modifier = Modifier.padding(start = 7.dp), fontSize = 13.sp)
        }
    }
}

@Composable
fun SendSection(viewModel: ChatViewModel, chatId: String, userId: Int) {
    val sendMessage = remember {
        mutableStateOf("")
    }
    val timestamp = remember {
        mutableStateOf<Long>(0)
    }
    Card(modifier = Modifier.fillMaxWidth()) {
        OutlinedTextField(
            value = sendMessage.value,
            onValueChange = { sendMessage.value = it },
            placeholder = { Text(text = "메세지를 작성해주세요") },
            trailingIcon = {
                IconButton(
                    onClick = {
                        // 메세지 보내기
                        if (sendMessage.value.isNotEmpty()) {
                            Log.d("새로운 메세지", sendMessage.value)
                            timestamp.value = System.currentTimeMillis()
                            // 
                            val message = RdbMessageData(sendMessage.value, false, timestamp.value, userId)
                            viewModel.newMessage(chatId, message)
                            sendMessage.value = ""
                        }
                    }
                ){
                    Icon(imageVector = Icons.Filled.Send,
                        contentDescription = "보내기", tint = colorResource(id = R.color.green))
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .padding(10.dp),
            colors = TextFieldDefaults.outlinedTextFieldColors(focusedBorderColor = colorResource(id = R.color.green))
        )
    }

Firebase DB

채팅룸

유저 채팅 목록

완성 화면

profile
찌니's develog

0개의 댓글