이번 포스팅에서는 애플리케이션 제작을 마무리하기 위한 작업들을 몇가지 해보도록 하겠습니다.
지금의 코드는 닉네임 또는 프로필을 변경한 후, 새로운 메시지를 보낼 때에는 변경 사항이 잘 반영되지만, 닉네임 또는 프로필을 변경하기 전에 보낸 메시지는 이전 닉네임 또는 프로필이 그대로 나온다. 따라서 닉네임과 프로필을 변경할 시, 변경 이전에 보낸 메시지라 할지라도, 새로운 닉네임과 프로필이 나타나도록 만들어주어야 한다.
ChatRoomActivity의 getMessageList 메서드를 아래와 같이 수정한다.
private fun getMessageList(chatRoomId: String) {
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
messageList.clear()
val newMessages = mutableListOf<MessageModel>() // 새로운 메시지를 저장할 리스트 생성
for (dataModel in dataSnapshot.children) {
val messageModel = dataModel.getValue(MessageModel::class.java)
val messageId = dataModel.key
if (messageModel != null) {
if (messageId != null) {
CoroutineScope(Dispatchers.IO).launch {
val response = getUserInfo(messageModel.senderUid)
if (response.isSuccess) {
if(response.result!!.imgUrl != null) {
FirebaseRef.message.child(chatRoomId!!).child(messageId).child("senderProfileUrl")
.setValue(response.result!!.imgUrl)
}
else {
FirebaseRef.message.child(chatRoomId!!).child(messageId).child("senderProfileUrl")
.setValue("null")
}
FirebaseRef.message.child(chatRoomId!!).child(messageId).child("senderNickName")
.setValue(response.result!!.nickName)
} else {
Log.d("NickNameActivity", "유저의 정보를 불러오지 못함")
}
}
// connectedUids로 업데이트
FirebaseRef.message.child(chatRoomId!!).child(messageId).child("readerUids")
.updateChildren(connectedUids as Map<String, Boolean>)
.addOnCompleteListener { readerUidTask ->
...
이제 코드를 실행시켜보면, 새로운 닉네임이나 프로필이 변경 이전 메시지에 서도 잘 적용될 것이다.
① 본인의 프로필을 클릭했을 때에 대한 이벤트 리스너도 등록해주자. 먼저 fragment_user_list.xml의 LinearLayout 태그에 id 속성을 추가한다.
android:id="@+id/myProfileLayout"
② friend 패키지 하위로, MyProfileActivity를 추가한다.
③ activity_my_profile.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"
android:background="@drawable/main_border"
tools:context=".friend.MyProfileActivity">
<ImageView
android:id="@+id/myProfileDetail"
android:layout_width="250dp"
android:layout_height="250dp"
android:src="@drawable/profile"
android:layout_marginTop="80dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/myNickNameDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="350dp"
android:text="NickName"
android:textStyle="bold"
android:textColor="#000000"
android:textSize="40sp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/modifyProfile"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/mypage"
android:layout_marginBottom="150dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="프로필 변경하기"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_marginTop="3dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toMyPage" />
</androidx.constraintlayout.widget.ConstraintLayout>
④ UserListFragment를 아래와 같이 수정한다.
var nickname: String? = null
var profileUrl: String? = null
...
val myProfileLayout = view.findViewById<LinearLayout>(R.id.myProfileLayout)
myProfileLayout.setOnClickListener {
val myImgUrl = profileUrl
val myNickName = nickname
val intent = Intent(requireActivity(), MyProfileActivity::class.java)
intent.putExtra("imgUrl", myImgUrl)
intent.putExtra("nickName", myNickName)
startActivity(intent)
}
⑤ MyProfileActivity에 아래의 내용을 입력한다.
class MyProfileActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my_profile)
// Intent로부터 데이터를 가져옴
val imgUrl = intent.getStringExtra("imgUrl")
val nickName = intent.getStringExtra("nickName")
val profileImageView = findViewById<ImageView>(R.id.myProfileDetail)
val nickNameTextView = findViewById<TextView>(R.id.myNickNameDetail)
val toMyPageBtn = findViewById<ImageView>(R.id.modifyProfile)
if(imgUrl != null) {
if(imgUrl != "null") {
Glide.with(this)
.load(imgUrl)
.into(profileImageView)
}
}
nickNameTextView.text = nickName
profileImageView.setOnClickListener {
val intent = Intent(this, ProfileImageActivity::class.java)
intent.putExtra("imgUrl", imgUrl)
startActivity(intent)
}
toMyPageBtn.setOnClickListener {
val intent = Intent(this, ProfileActivity::class.java)
startActivity(intent)
}
}
}
코드를 실행해보면 본인의 상세 프로필이 잘 나올 것이며, 사진을 클릭하여 확대해 볼 수도 있다. 아래의 프로필 변경하기 버튼을 클릭하면 ProfileActivity로 전환된다.
① UserListFragment에서 선택한 유저의 uid를 Intent로 UserDetailActivity에 전달한다.
listview.setOnItemClickListener { parent, view, position, id ->
val uid = userProfileList!![position].uid
val imgUrl = userProfileList!![position].imgUrl
val nickName = userProfileList!![position].nickName
val intent = Intent(requireActivity(), UserDetailActivity::class.java)
intent.putExtra("uid", uid)
intent.putExtra("imgUrl", imgUrl)
intent.putExtra("nickName", nickName)
startActivity(intent)
}
② UserDetailActivity를 아래와 같이 수정한다.
class UserDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_detail)
// Intent로부터 데이터를 가져옴
val uid = intent.getStringExtra("uid")
val imgUrl = intent.getStringExtra("imgUrl")
val nickName = intent.getStringExtra("nickName")
val profileImageView = findViewById<ImageView>(R.id.profileDetail)
val nickNameTextView = findViewById<TextView>(R.id.nickNameDetail)
val startChatBtn = findViewById<ImageView>(R.id.startChat)
if(imgUrl != null) {
Glide.with(this)
.load(imgUrl)
.into(profileImageView)
}
nickNameTextView.text = nickName
profileImageView.setOnClickListener {
val intent = Intent(this, ProfileImageActivity::class.java)
intent.putExtra("imgUrl", imgUrl)
startActivity(intent)
}
startChatBtn.setOnClickListener {
showDialog(uid!!)
}
}
private fun getAccessToken(callback: (String) -> Unit) {
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val data = dataSnapshot.getValue(com.chrome.chattingapp.authentication.UserInfo::class.java)
val accessToken = data?.accessToken ?: ""
callback(accessToken)
}
override fun onCancelled(databaseError: DatabaseError) {
Log.w("ChatListFragment", "onCancelled", databaseError.toException())
}
}
FirebaseRef.userInfo.child(FirebaseAuthUtils.getUid()).addListenerForSingleValueEvent(postListener)
}
private suspend fun createChatRoom(accessToken : String, roomName : String): BaseResponse<String> {
return RetrofitInstance.chatApi.createChatRoom(accessToken, roomName)
}
private fun showDialog(uid : String) {
val dialogView = LayoutInflater.from(this).inflate(R.layout.chat_room_dialog, null)
val builder = AlertDialog.Builder(this)
.setView(dialogView)
.setTitle("채팅방 생성하기")
val alertDialog = builder.show()
val createBtn = alertDialog.findViewById<Button>(R.id.create)
createBtn.setOnClickListener {
val roomName = alertDialog.findViewById<TextInputEditText>(R.id.roomName)
val roomNameStr = roomName.text.toString()
if(roomNameStr.isEmpty()) {
Toast.makeText(this, "채팅방 이름을 입력해주세요", Toast.LENGTH_SHORT).show()
}
else if(roomNameStr.length > 12) {
Toast.makeText(this, "채팅방 이름은 12글자 미만으로 입력해주세요", Toast.LENGTH_SHORT).show()
}
else {
getAccessToken { accessToken ->
if (accessToken.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
val response = createChatRoom(accessToken, roomNameStr)
if (response.isSuccess) {
Log.d("ChatListFragment", response.toString())
val roomId = response.result
val chatRoom = ChatRoom(roomId!!, roomNameStr)
Log.d("ChatRoom", chatRoom.toString())
// 본인의 채팅방
FirebaseRef.chatRoom.child(FirebaseAuthUtils.getUid()).child(roomId!!).setValue(chatRoom)
// 초대 받은 사람을 위한 채팅방
FirebaseRef.chatRoom.child(uid).child(roomId!!).setValue(chatRoom)
val intent = Intent(this@UserDetailActivity, ChatRoomActivity::class.java)
intent.putExtra("chatRoomId", roomId)
intent.putExtra("chatRoomName", roomNameStr)
intent.putExtra("invitedUid", uid)
var invitedNickName : String? = null
CoroutineScope(Dispatchers.IO).launch {
val addUserReq = AddUserReq(uid, roomId)
val response = addUser(addUserReq)
if (response.isSuccess) {
invitedNickName = response.result
FirebaseRef.chatRoom.child(uid).child(roomId).setValue(chatRoom)
startActivity(intent)
} else {
val message = response.message
Log.d("UserDetailActivity", message)
withContext(Dispatchers.Main) {
Toast.makeText(this@UserDetailActivity, message, Toast.LENGTH_SHORT).show()
}
}
}
withContext(Dispatchers.Main) {
Toast.makeText(this@UserDetailActivity, invitedNickName + "님과 채팅을 시작합니다", Toast.LENGTH_SHORT).show()
}
}
else {
Log.d("ChatListFragment", "채팅방 생성 실패")
val message = response.message
Log.d("ChatListFragment", message)
withContext(Dispatchers.Main) {
Toast.makeText(this@UserDetailActivity, message, Toast.LENGTH_SHORT).show()
}
}
}
} else {
Log.e("ChatListFragment", "Invalid Token")
}
}
alertDialog.dismiss()
}
}
}
private suspend fun addUser(addUserReq: AddUserReq) : BaseResponse<String> {
return RetrofitInstance.chatApi.addUser(addUserReq)
}
}
이제 코드를 실행해보면, 유저 상세 정보 창의 채팅하기 버튼으로 해당 유저와의 채팅방을 생성할 수 있다.
채팅방 메시지에서 유저의 프로필 사진을 클릭하면 유저의 상세 정보 페이지가 나오게 해보겠다. RecyclerView의 경우 ListView보다 효율적이기는 하지만, ItemClickListener를 등록하는 과정은 ListView에 비해 까다로운 편이다.
① MessageAdapter를 아래와 같이 수정한다.
...
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.itemView.setOnClickListener {
itemClickListener.onClick(it, position)
}
}
MessageModel.VIEW_TYPE_YOU -> {
(holder as ItemViewHolder2).contents.text = item.contents
...
holder.itemView.setOnClickListener {
itemClickListener.onClick(it, position)
}
}
}
}
...
interface OnItemClickListener {
fun onClick(view : View, position : Int)
}
fun setItemClickListener(onItemClickListener: OnItemClickListener) {
this.itemClickListener = onItemClickListener
}
private lateinit var itemClickListener : OnItemClickListener
}
② ChatRoomActivity를 아래의 내용을 추가한다.
messageAdapter.setItemClickListener(object : MessageAdapter.OnItemClickListener {
override fun onClick(view: View, position: Int) {
val uid = messageList!![position].senderUid
val imgUrl = messageList!![position].senderProfileUrl
val nickName = messageList!![position].senderNickName
var intent : Intent? = null
if(uid == FirebaseAuthUtils.getUid()) {
intent = Intent(this@ChatRoomActivity, MyProfileActivity::class.java)
intent.putExtra("imgUrl", imgUrl)
intent.putExtra("nickName", nickName)
}
else {
intent = Intent(this@ChatRoomActivity, UserDetailActivity::class.java)
intent.putExtra("uid", uid)
intent.putExtra("imgUrl", imgUrl)
intent.putExtra("nickName", nickName)
}
startActivity(intent)
}
})
이제 채팅방 메시지의 프로필에서도 유저의 정보를 조회할 수 있게 된다.
지금까지는 프로필에 사용되는 이미지의 크기가 제각각이다보니 프로필 이미지를 일관된 모양으로 보여주지 못했다. 이제부터는 이미지를 동그랗게 만들어 모든 사진이 동일한 사이즈로 보여지도록 만들어주겠다.
① Module 수준의 build.gradle 파일에 아래의 의존성을 추가한다.
implementation("de.hdodenhof:circleimageview:3.1.0")
② 프로필 이미지가 나타나는 모든 ImageView 태그를 아래의 de.hdodenhof.circleimageview.CircleImageView로 변경한다.
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/listProfileArea"
android:layout_width="90dp"
android:layout_height="90dp"
android:src="@drawable/profile"
android:layout_marginVertical="10dp"
android:layout_marginLeft="20dp"
app:civ_border_color="@color/skyBlue"
app:civ_border_overlay="true"
app:civ_border_width="1dp" />
이제 유저의 프로필 이미지의 크기와 무관하게 균일한 크기의 동그란 이미지로 나타날 것이다.
이로써 실시간 채팅 어플리케이션, U & I TALK가 모두 완성되었다. 기본적인 내용들은 거의 다 다룬 것 같으니, 다음 포스팅부터는 조금 더 심화적인 내용을 다뤄보기로 하자.
앱을 만드는 기본기만 다지고 싶다면, 여기까지만 진행해도 무방하고, 조금 더 알아보고 싶다면 심화 과정도 연습해보기 바란다.