-
[Android]Firebase로 채팅 앱 만들기(4)Andorid 2022. 3. 31. 14:55
회원가입 페이지와 로그인 페이지도 만들어졌으니,
이제 진입할 홈 화면을 만들 차례다.
나는 카카오톡처럼 채팅방 목록과 함께 새 채팅방을 추가할 수 있는 버튼과 로그아웃 버튼을 표시했다.
<?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" android:background="@color/white" tools:context=".main.MainActivity"> <TextView android:id="@+id/txt_TItle" style="@style/boldBlack" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="30dp" android:text="메시지" android:textSize="18dp" app:layout_constraintEnd_toStartOf="@+id/btn_new_message" app:layout_constraintStart_toEndOf="@+id/btn_signout" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btn_new_message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="20dp" android:layout_marginRight="20dp" android:background="@android:color/transparent" android:text="새 메시지" android:textColor="#3F51B5" android:textSize="18dp" android:textStyle="normal" app:layout_constraintBottom_toBottomOf="@+id/txt_TItle" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/txt_TItle" /> <Button android:id="@+id/btn_signout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" android:text="로그아웃" android:textColor="#8D8D8E" android:textSize="18dp" android:textStyle="normal" app:layout_constraintBottom_toBottomOf="@+id/txt_TItle" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/txt_TItle" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_chatrooms" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="40dp" android:layout_marginBottom="40dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/txt_TItle" tools:listitem="@layout/list_chatroom_item" /> </androidx.constraintlayout.widget.ConstraintLayout>
전체 레이아웃 코드는 위와 같다.
package com.miso.chatapplication.main import android.content.Intent import android.os.Build import android.os.Bundle import android.widget.Button import android.widget.Toast import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.firebase.auth.FirebaseAuth import com.google.firebase.database.* import com.miso.chatapplication.LoginActivity import com.miso.chatapplication.addChatRoom.AddChatRoomActivity import com.miso.chatapplication.databinding.ActivityMainBinding import com.miso.chatapplication.model.ChatRoom @RequiresApi(Build.VERSION_CODES.O) class MainActivity : AppCompatActivity() { lateinit var btnAddchatRoom: Button lateinit var btnSignout: Button lateinit var binding: ActivityMainBinding lateinit var firebaseDatabase: DatabaseReference lateinit var recycler_chatroom: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) initializeView() initializeListener() setupRecycler() } fun initializeView() { //뷰 초기화 try { firebaseDatabase = FirebaseDatabase.getInstance().getReference("ChatRoom")!! btnSignout = binding.btnSignout btnAddchatRoom = binding.btnNewMessage recycler_chatroom = binding.recyclerChatrooms }catch (e:Exception) { e.printStackTrace() Toast.makeText(this,"화면 초기화 중 오류가 발생하였습니다.",Toast.LENGTH_LONG).show() } } fun initializeListener() //버튼 클릭 시 리스너 초기화 { btnSignout.setOnClickListener() { signOut() } btnAddchatRoom.setOnClickListener() //새 메시지 화면으로 이동 { startActivity(Intent(this@MainActivity, AddChatRoomActivity::class.java)) finish() } } fun setupRecycler() { recycler_chatroom.layoutManager = LinearLayoutManager(this) recycler_chatroom.adapter = RecyclerChatRoomsAdapter(this) } fun signOut() //로그아웃 실행 { try { val builder = AlertDialog.Builder(this) .setTitle("로그아웃") .setMessage("로그아웃 하시겠습니까?") .setPositiveButton("확인" ) { dialog, id -> try { FirebaseAuth.getInstance().signOut() //로그아웃 startActivity(Intent(this@MainActivity, LoginActivity::class.java)) dialog.dismiss() finish() } catch (e: Exception) { e.printStackTrace() dialog.dismiss() Toast.makeText(this, "로그아웃 중 오류가 발생하였습니다.", Toast.LENGTH_LONG).show() } } .setNegativeButton("취소" //다이얼로그 닫기 ) { dialog, id -> dialog.dismiss() } builder.show() }catch (e:Exception) { e.printStackTrace() Toast.makeText(this,"로그아웃 중 오류가 발생하였습니다.",Toast.LENGTH_LONG).show() } } override fun onBackPressed() { signOut() } }
액티비티 전체 코드는 위와 같다.
fun signOut() //로그아웃 실행 { try { val builder = AlertDialog.Builder(this) .setTitle("로그아웃") .setMessage("로그아웃 하시겠습니까?") .setPositiveButton("확인" ) { dialog, id -> try { FirebaseAuth.getInstance().signOut() //로그아웃 startActivity(Intent(this@MainActivity, LoginActivity::class.java)) dialog.dismiss() finish() } catch (e: Exception) { e.printStackTrace() dialog.dismiss() Toast.makeText(this, "로그아웃 중 오류가 발생하였습니다.", Toast.LENGTH_LONG).show() } } .setNegativeButton("취소" //다이얼로그 닫기 ) { dialog, id -> dialog.dismiss() } builder.show() }catch (e:Exception) { e.printStackTrace() Toast.makeText(this,"로그아웃 중 오류가 발생하였습니다.",Toast.LENGTH_LONG).show() } } override fun onBackPressed() { signOut() }
로그아웃 버튼을 눌렀을 시 다이얼로그를 띄우도록 하고,
확인 버튼을 누를 시 로그아웃을 실행한 후 로그인 화면으로 이동하도록 했고,
하단의 백 버튼을 눌렀을 때에도 동일하게 동작되도록 했다.
채팅방을 불러와 목록으로 표시하는 로직은 별도의 RecyclerAdapter를 만들어 수행하기로 했다.
그 전에,각 채팅방에서 사용할 정보를 담을 채팅방 객체를 만들어야 한다.
package com.miso.chatapplication.model import android.os.Parcelable import java.io.Serializable data class ChatRoom( val users: Map<String, Boolean>? = HashMap(), var messages: Map<String,Message>? = HashMap() ) : Serializable { }
채팅방의 정보를 저장하는 ChatRoom 객체이다.
Firebase RealtimeDatabase는 JSON 형태로 데이터를 저장하기 떄문에,
Map형태로 데이터를 전달하는 것이 좋다.
나는 일전에 값을 List형태로 전달해보았으나 런타임 오류로 실패했기에,Map을 전달하는 것을 권장한다.
채팅방에 포함된 사용자는 users에 저장하고,
해당 채팅방에서 오간 메시지는 messages에 저장하기로 하였다.
package com.miso.chatapplication.model import java.io.Serializable data class Message( var senderUid: String = "", var sended_date: String = "", var content: String = "", var confirmed:Boolean=false ) : Serializable { }
채팅방에서 오간 메시지를 저장하는 객체다.
차례대로 보낸 사람의 uid,
보낸 시각,
메시지의 내용,
상대방의 확인 여부를 저장한다.
그리고 각 data class는 인텐트로 전달될 필요가 있어
Serializable을 구현하도록 하였다.
@RequiresApi(Build.VERSION_CODES.O) class RecyclerChatRoomsAdapter(val context: Context) : RecyclerView.Adapter<RecyclerChatRoomsAdapter.ViewHolder>() { var chatRooms: ArrayList<ChatRoom> = arrayListOf() //채팅방 목록 var chatRoomKeys: ArrayList<String> = arrayListOf() //채팅방 키 목록 val myUid = FirebaseAuth.getInstance().currentUser?.uid.toString() //현재 사용자 Uid init { setupAllUserList() } fun setupAllUserList() { //전체 채팅방 목록 초기화 및 업데이트 FirebaseDatabase.getInstance().getReference("ChatRoom").child("chatRooms") .orderByChild("users/$myUid").equalTo(true) .addListenerForSingleValueEvent(object : ValueEventListener { override fun onCancelled(error: DatabaseError) {} override fun onDataChange(snapshot: DataSnapshot) { chatRooms.clear() for (data in snapshot.children) { chatRooms.add(data.getValue<ChatRoom>()!!) chatRoomKeys.add(data.key!!) } notifyDataSetChanged() } }) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(context).inflate(R.layout.list_chatroom_item, parent, false) return ViewHolder(ListChatroomItemBinding.bind(view)) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { var userIdList = chatRooms[position].users!!.keys //채팅방에 포함된 사용자 키 목록 var opponent = userIdList.first { !it.equals(myUid) } //상대방 사용자 키 FirebaseDatabase.getInstance().getReference("User").child("users").orderByChild("uid") //상대방 사용자 키를 포함하는 채팅방 불러오기 .equalTo(opponent) .addListenerForSingleValueEvent(object : ValueEventListener { override fun onCancelled(error: DatabaseError) {} override fun onDataChange(snapshot: DataSnapshot) { for (data in snapshot.children) { holder.chatRoomKey = data.key.toString()!! //채팅방 키 초기화 holder.opponentUser = data.getValue<User>()!! //상대방 정보 초기화 holder.txt_name.text = data.getValue<User>()!!.name.toString() //상대방 이름 초괴화 } } }) holder.background.setOnClickListener() //채팅방 항목 선택 시 { try { var intent = Intent(context, ChatRoomActivity::class.java) intent.putExtra("ChatRoom", chatRooms.get(position)) //채팅방 정보 intent.putExtra("Opponent", holder.opponentUser) //상대방 사용자 정보 intent.putExtra("ChatRoomKey", chatRoomKeys[position]) //채팅방 키 정보 context.startActivity(intent) //해당 채팅방으로 이동 (context as AppCompatActivity).finish() }catch (e:Exception) { e.printStackTrace() Toast.makeText(context,"채팅방 이동 중 문제가 발생하였습니다.",Toast.LENGTH_SHORT).show() } } if (chatRooms[position].messages!!.size > 0) { //채팅방 메시지가 존재하는 경우 setupLastMessageAndDate(holder, position) //마지막 메시지 및 시각 초기화 setupMessageCount(holder, position) } } fun setupLastMessageAndDate(holder: ViewHolder, position: Int) { //마지막 메시지 및 시각 초기화 try { var lastMessage = chatRooms[position].messages!!.values.sortedWith(compareBy({ it.sended_date })) //메시지 목록에서 시각을 비교하여 가장 마지막 메시지 가져오기 .last() holder.txt_message.text = lastMessage.content //마지막 메시지 표시 holder.txt_date.text = getLastMessageTimeString(lastMessage.sended_date) //마지막으로 전송된 시각 표시 } catch (e: Exception) { e.printStackTrace() } } fun setupMessageCount(holder: ViewHolder, position: Int) { //확인되지 않은 메시지 개수 표시 try { var unconfirmedCount = chatRooms[position].messages!!.filter { !it.value.confirmed && !it.value.senderUid.equals( //메시지 중 확인되지 않은 메시지 개수 가져오기 myUid ) }.size if (unconfirmedCount > 0) { //확인되지 않은 메시지가 있을 경우 holder.txt_chatCount.visibility = View.VISIBLE //개수 표시 holder.txt_chatCount.text = unconfirmedCount.toString() } else holder.txt_chatCount.visibility = View.GONE } catch (e: Exception) { e.printStackTrace() holder.txt_chatCount.visibility = View.GONE } } fun getLastMessageTimeString(lastTimeString: String): String { //마지막 메시지가 전송된 시각 구하기 try { var currentTime = LocalDateTime.now().atZone(TimeZone.getDefault().toZoneId()) //현재 시각 var dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss") var messageMonth = lastTimeString.substring(4, 6).toInt() //마지막 메시지 시각 월,일,시,분 var messageDate = lastTimeString.substring(6, 8).toInt() var messageHour = lastTimeString.substring(8, 10).toInt() var messageMinute = lastTimeString.substring(10, 12).toInt() var formattedCurrentTimeString = currentTime.format(dateTimeFormatter) //현 시각 월,일,시,분 var currentMonth = formattedCurrentTimeString.substring(4, 6).toInt() var currentDate = formattedCurrentTimeString.substring(6, 8).toInt() var currentHour = formattedCurrentTimeString.substring(8, 10).toInt() var currentMinute = formattedCurrentTimeString.substring(10, 12).toInt() var monthAgo = currentMonth - messageMonth //현 시각과 마지막 메시지 시각과의 차이. 월,일,시,분 var dayAgo = currentDate - messageDate var hourAgo = currentHour - messageHour var minuteAgo = currentMinute - messageMinute if (monthAgo > 0) //1개월 이상 차이 나는 경우 return monthAgo.toString() + "개월 전" else { if (dayAgo > 0) { //1일 이상 차이 나는 경우 if (dayAgo == 1) return "어제" else return dayAgo.toString() + "일 전" } else { if (hourAgo > 0) return hourAgo.toString() + "시간 전" //1시간 이상 차이 나는 경우 else { if (minuteAgo > 0) //1분 이상 차이 나는 경우 return minuteAgo.toString() + "분 전" else return "방금" } } } } catch (e: Exception) { e.printStackTrace() return "" } } override fun getItemCount(): Int { return chatRooms.size } inner class ViewHolder(itemView: ListChatroomItemBinding) : RecyclerView.ViewHolder(itemView.root) { var opponentUser = User("", "") var chatRoomKey = "" var background = itemView.background var txt_name = itemView.txtName var txt_message = itemView.txtMessage var txt_date = itemView.txtMessageDate var txt_chatCount = itemView.txtChatCount } }
채팅방을 불러오는 RecyclerChatRoomsAdapter의 전체 코드는 위와 같다.
var chatRooms: ArrayList<ChatRoom> = arrayListOf() //채팅방 목록 var chatRoomKeys: ArrayList<String> = arrayListOf() //채팅방 키 목록 val myUid = FirebaseAuth.getInstance().currentUser?.uid.toString() //현재 사용자 Uid init { setupAllUserList() }
우선 첫째로 채팅방과 각 채팅방의 키를 받아올 채팅방 키를 저장할 ArrayList들과,
현재 로그인한 사용자의 Uid를 저장할 변수를 선언하였고,
생성과 동시에 전체 채팅방 목록을 초기화하도록 하였다.
fun setupAllUserList() { //전체 채팅방 목록 초기화 및 업데이트 FirebaseDatabase.getInstance().getReference("ChatRoom").child("chatRooms") .orderByChild("users/$myUid").equalTo(true) .addListenerForSingleValueEvent(object : ValueEventListener { override fun onCancelled(error: DatabaseError) {} override fun onDataChange(snapshot: DataSnapshot) { chatRooms.clear() for (data in snapshot.children) { chatRooms.add(data.getValue<ChatRoom>()!!) chatRoomKeys.add(data.key!!) } notifyDataSetChanged() } }) }
채팅방 데이터를 저장하고 있는 ChatRoom 항목에서 자신이 포함된 채팅방들만 불러오기 위하여
myUid를 child로 가지고 있는 항목만 추려온 뒤, chatRooms와 chatRoomKey에 각각 항목을 추가한다.
override fun onBindViewHolder(holder: ViewHolder, position: Int) { var userIdList = chatRooms[position].users!!.keys //채팅방에 포함된 사용자 키 목록 var opponent = userIdList.first { !it.equals(myUid) } //상대방 사용자 키 FirebaseDatabase.getInstance().getReference("User").child("users").orderByChild("uid") //상대방 사용자 키를 포함하는 채팅방 불러오기 .equalTo(opponent) .addListenerForSingleValueEvent(object : ValueEventListener { override fun onCancelled(error: DatabaseError) {} override fun onDataChange(snapshot: DataSnapshot) { for (data in snapshot.children) { holder.chatRoomKey = data.key.toString()!! //채팅방 키 초기화 holder.opponentUser = data.getValue<User>()!! //상대방 정보 초기화 holder.txt_name.text = data.getValue<User>()!!.name.toString() //상대방 이름 초괴화 } } }) holder.background.setOnClickListener() //채팅방 항목 선택 시 { try { var intent = Intent(context, ChatRoomActivity::class.java) intent.putExtra("ChatRoom", chatRooms.get(position)) //채팅방 정보 intent.putExtra("Opponent", holder.opponentUser) //상대방 사용자 정보 intent.putExtra("ChatRoomKey", chatRoomKeys[position]) //채팅방 키 정보 context.startActivity(intent) //해당 채팅방으로 이동 (context as AppCompatActivity).finish() }catch (e:Exception) { e.printStackTrace() Toast.makeText(context,"채팅방 이동 중 문제가 발생하였습니다.",Toast.LENGTH_SHORT).show() } } if (chatRooms[position].messages!!.size > 0) { //채팅방 메시지가 존재하는 경우 setupLastMessageAndDate(holder, position) //마지막 메시지 및 시각 초기화 setupMessageCount(holder, position) } }
초기화된 각 채팅방에서 상대방 사용자의 키를 얻어내고, 사용자 목록이 포함된 Users 데이터베이스에서 상대방 사용자를 찾아 정보를 불러온 뒤, 상대방의 이름을 초기화하여 보여준다.
그리고 해당 채팅방 항목을 선택 시, 해당 상대방과의 채팅방 정보, 상대방 사용자의 정보, 키를 Intent로 넘겨 채팅방 화면으로 전환하도록 한다.
그리고 채팅방에 메시지가 존재하는 경우, 마지막 메시지와 메시지를 전송한 시각도 함께 표시해준다.
fun setupLastMessageAndDate(holder: ViewHolder, position: Int) { //마지막 메시지 및 시각 초기화 try { var lastMessage = chatRooms[position].messages!!.values.sortedWith(compareBy({ it.sended_date })) //메시지 목록에서 시각을 비교하여 가장 마지막 메시지 가져오기 .last() holder.txt_message.text = lastMessage.content //마지막 메시지 표시 holder.txt_date.text = getLastMessageTimeString(lastMessage.sended_date) //마지막으로 전송된 시각 표시 } catch (e: Exception) { e.printStackTrace() } }
해당 채팅방에 포함된 메시지들을 비교해서 시각이 가장 큰 메시지를 뽑아 텍스트 표시
fun getLastMessageTimeString(lastTimeString: String): String { //마지막 메시지가 전송된 시각 구하기 try { var currentTime = LocalDateTime.now().atZone(TimeZone.getDefault().toZoneId()) //현재 시각 var dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss") var messageMonth = lastTimeString.substring(4, 6).toInt() //마지막 메시지 시각 월,일,시,분 var messageDate = lastTimeString.substring(6, 8).toInt() var messageHour = lastTimeString.substring(8, 10).toInt() var messageMinute = lastTimeString.substring(10, 12).toInt() var formattedCurrentTimeString = currentTime.format(dateTimeFormatter) //현 시각 월,일,시,분 var currentMonth = formattedCurrentTimeString.substring(4, 6).toInt() var currentDate = formattedCurrentTimeString.substring(6, 8).toInt() var currentHour = formattedCurrentTimeString.substring(8, 10).toInt() var currentMinute = formattedCurrentTimeString.substring(10, 12).toInt() var monthAgo = currentMonth - messageMonth //현 시각과 마지막 메시지 시각과의 차이. 월,일,시,분 var dayAgo = currentDate - messageDate var hourAgo = currentHour - messageHour var minuteAgo = currentMinute - messageMinute if (monthAgo > 0) //1개월 이상 차이 나는 경우 return monthAgo.toString() + "개월 전" else { if (dayAgo > 0) { //1일 이상 차이 나는 경우 if (dayAgo == 1) return "어제" else return dayAgo.toString() + "일 전" } else { if (hourAgo > 0) return hourAgo.toString() + "시간 전" //1시간 이상 차이 나는 경우 else { if (minuteAgo > 0) //1분 이상 차이 나는 경우 return minuteAgo.toString() + "분 전" else return "방금" } } } } catch (e: Exception) { e.printStackTrace() return "" } }
메시지 시각을 오늘 날짜와 비교하여 마지막으로 전송된 메시지를 다음과 같이 표시한다.
ex)3분 전, 10분 전, 어제,3일 전..
fun setupMessageCount(holder: ViewHolder, position: Int) { //확인되지 않은 메시지 개수 표시 try { var unconfirmedCount = chatRooms[position].messages!!.filter { !it.value.confirmed && !it.value.senderUid.equals( //메시지 중 확인되지 않은 메시지 개수 가져오기 myUid ) }.size if (unconfirmedCount > 0) { //확인되지 않은 메시지가 있을 경우 holder.txt_chatCount.visibility = View.VISIBLE //개수 표시 holder.txt_chatCount.text = unconfirmedCount.toString() } else holder.txt_chatCount.visibility = View.GONE } catch (e: Exception) { e.printStackTrace() holder.txt_chatCount.visibility = View.GONE } }
채팅방의 메시지 중에서 확인되지 않은 메시지의 개수를 세어 표시해준다.
완성된 화면이다.
참조
https://m.blog.naver.com/zoown13/222094002924https://cionman.tistory.com/72https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=traeumen927&logNo=221493556497https://lasbe.tistory.com/19
'Andorid' 카테고리의 다른 글
[Android]Firebase로 채팅 앱 만들기(6) (0) 2022.03.31 [Android]Firebase로 채팅 앱 만들기(5) (0) 2022.03.31 [Android]Firebase로 채팅 앱 만들기(3) (0) 2022.03.31 [Android]Firebase로 채팅 앱 만들기(2) (0) 2022.03.31 [Android] Firebase로 채팅 앱 만들기(1) (0) 2022.03.31