마무리 - 프로필 동기화 & ClickListener & CircleView

변현섭·2023년 9월 13일
0
post-thumbnail

이번 포스팅에서는 애플리케이션 제작을 마무리하기 위한 작업들을 몇가지 해보도록 하겠습니다.

1. 닉네임 & 프로필 동기화

지금의 코드는 닉네임 또는 프로필을 변경한 후, 새로운 메시지를 보낼 때에는 변경 사항이 잘 반영되지만, 닉네임 또는 프로필을 변경하기 전에 보낸 메시지는 이전 닉네임 또는 프로필이 그대로 나온다. 따라서 닉네임과 프로필을 변경할 시, 변경 이전에 보낸 메시지라 할지라도, 새로운 닉네임과 프로필이 나타나도록 만들어주어야 한다.

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 ->
                        
                        ...

이제 코드를 실행시켜보면, 새로운 닉네임이나 프로필이 변경 이전 메시지에 서도 잘 적용될 것이다.

2. Friend 탭의 채팅 버튼 ClickListener

1) 내 프로필에 대한 클릭 이벤트 리스너

① 본인의 프로필을 클릭했을 때에 대한 이벤트 리스너도 등록해주자. 먼저 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로 전환된다.

2) 채팅하기 버튼에 대한 클릭 이벤트 리스너

① 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)
    }
}

이제 코드를 실행해보면, 유저 상세 정보 창의 채팅하기 버튼으로 해당 유저와의 채팅방을 생성할 수 있다.

3. 채팅 메시지의 ItemClickListener

채팅방 메시지에서 유저의 프로필 사진을 클릭하면 유저의 상세 정보 페이지가 나오게 해보겠다. 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를 아래의 내용을 추가한다.

  • 본인의 프로필을 선택했을 때에는 UserDetailActivity가 아닌, MyProfileActivity를 열어야 한다.
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)
    }
})

이제 채팅방 메시지의 프로필에서도 유저의 정보를 조회할 수 있게 된다.

3. 프로필 이미지에 CircleView 적용하기

지금까지는 프로필에 사용되는 이미지의 크기가 제각각이다보니 프로필 이미지를 일관된 모양으로 보여주지 못했다. 이제부터는 이미지를 동그랗게 만들어 모든 사진이 동일한 사이즈로 보여지도록 만들어주겠다.

① Module 수준의 build.gradle 파일에 아래의 의존성을 추가한다.

implementation("de.hdodenhof:circleimageview:3.1.0")

② 프로필 이미지가 나타나는 모든 ImageView 태그를 아래의 de.hdodenhof.circleimageview.CircleImageView로 변경한다.

  • border_overlay는 테두리 여부, app:civ_border_width는 테두리 두께를 의미한다.
  • 각 상황에 맞게 id, width, height, margin, src를 변경한다.
<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가 모두 완성되었다. 기본적인 내용들은 거의 다 다룬 것 같으니, 다음 포스팅부터는 조금 더 심화적인 내용을 다뤄보기로 하자.

앱을 만드는 기본기만 다지고 싶다면, 여기까지만 진행해도 무방하고, 조금 더 알아보고 싶다면 심화 과정도 연습해보기 바란다.

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글