iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0
Mobile Development

30天建立寵物約散App-Android新手篇系列 第 19

【day19】聊天室(下) X Realtime database

好的,那接下來我們就要來顯示我們的資料啦!! 由於我們的資料會有一個是對方傳過來的,一個是我們自己發送過去的,而時間的先後順序Realtime那邊會按照先後順序幫我們排序起來,所以我們不用去管它。

一、建立畫面

既然提到了recyclerview,我們當然要有item_view_list阿,而且因為我們這次會有一個是對方傳過來的,一個是我們傳過去的(簡單分法就是一個在左,一個在右),所以我們會建立兩個item_view_list。

1.先建立來自我們自己的畫面

簡單明瞭,帶點line的風格

https://ithelp.ithome.com.tw/upload/images/20211004/20138017szDx67chaE.png

<?xml version="1.0" encoding="utf-8"?>


<layout 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">

    <data>
        <variable
            name="message"
            type="com.example.petsmatchingapp.model.Message" />
    </data>



    <RelativeLayout
        android:padding="5dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">



            <com.example.petsmatchingapp.utils.JFTextView
                android:id="@+id/tv_item_message_me_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toStartOf="@id/tv_item_message_me_message"
                android:layout_alignBottom="@id/tv_item_message_me_message"
                android:layout_marginEnd="5dp"
                android:textSize="12sp"
                tools:text = "13:00">


            </com.example.petsmatchingapp.utils.JFTextView>

            <com.example.petsmatchingapp.utils.JFTextView
                android:id="@+id/tv_item_message_me_message"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{message.message}"
                android:layout_alignParentEnd="true"
                android:padding="5dp"
                android:textSize="16sp"
                android:maxWidth="250dp"
                android:background="@drawable/message_background_me"
                tools:text = "你今天吃飽了嗎?">


            </com.example.petsmatchingapp.utils.JFTextView>


    </RelativeLayout>
</layout>

這次我們用RelativeLayout的原因是因為,我原本用constraintlayout,就是不能從右邊排版,所以我們這次用RelativeLayout的android:layout_alignParentEnd="true"

也要新增我們message的background喔

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">


    <solid
        android:color="@color/message_me_color">
    </solid>

    <corners
        android:radius="20dp">

    </corners>



</shape>

綠色顏色代碼:#78e37a

2.來自對方的訊息

https://ithelp.ithome.com.tw/upload/images/20211004/20138017RnlXMQpHl4.png

<?xml version="1.0" encoding="utf-8"?>

<layout 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">


    <data>
        <variable
            name="message"
            type="com.example.petsmatchingapp.model.Message" />


    </data>

    <RelativeLayout

        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp">


        <ImageView
            android:id="@+id/iv_item_message_other_image"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_alignParentStart="true"
            tools:src = "@drawable/icon_dog"/>



        <com.example.petsmatchingapp.utils.JFTextView
            android:id="@+id/item_message_other_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="5dp"
            android:text="@{message.message}"
            android:padding="5dp"
            android:maxWidth="250dp"
            android:textSize="16sp"
            android:layout_toEndOf="@id/iv_item_message_other_image"
            android:layout_alignTop="@id/iv_item_message_other_image"
            android:background="@drawable/message_backgorund_other"
            tools:text = "今天我吃飽了喔,我想應該也還沒吃飽啦"/>

        <com.example.petsmatchingapp.utils.JFTextView
            android:id="@+id/item_message_other_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toEndOf="@id/item_message_other_message"
            android:layout_alignBottom="@id/item_message_other_message"
            android:layout_marginStart="5dp"
            tools:text="03:05 pm"
            android:textSize="12sp"
            app:layout_constraintStart_toEndOf="@id/item_message_other_message">

        </com.example.petsmatchingapp.utils.JFTextView>


    </RelativeLayout>

</layout>

background只是把剛剛的綠色改成白色

二、建立Adapter

跟以往的adapter不一樣,我們這次因為有兩個item_view_list,所以我們這次會在adapter來判斷,而判斷的依據就是這個message的資料的發送者是不是現在的使用者

直接在Adapter的class新增以下,讓我們可以判斷要用哪一個viewHolder

private val MESSAGE_TYPE_LEFT = 0
private val MESSAGE_TYPE_RIGHT = 1

private var firebaseUser: FirebaseUser? = null

再過來就是override以下,來判斷是否是現在登入的user

override fun getItemViewType(position: Int): Int {
        firebaseUser = FirebaseAuth.getInstance().currentUser
        if (firebaseUser?.uid == getItem(position).send_user_id){
            return MESSAGE_TYPE_RIGHT
        }else{
            return MESSAGE_TYPE_LEFT
        }
    }

再過來要新增兩個viewHolder,一個是我們傳過去的,一個是別人傳過來的
首先先做我們傳出去的

class MyMessageViewHolder(val binding: MessageItemListMeBinding):RecyclerView.ViewHolder(binding.root){

		//這個funtion是我們要轉換時間的
        fun getDate(timestamp: Any): String{
			//我們只要小時跟分鐘就好
            val mFormat = "HH:mm"
            val sdf = SimpleDateFormat(mFormat, Locale.getDefault())
            return sdf.format(timestamp)
        }
        //一樣綁定資料,只有時間因為需要轉換,所以我們寫在adapter
        fun bind(item:Message){
            binding.message = item
            item.time?.let {
                binding.tvItemMessageMeTime.text = getDate(it)
            }

            binding.executePendingBindings()
        }
    }

別人傳過來的一樣

class OtherMessageViewHolder(val binding: MessageListItemOtherBinding): RecyclerView.ViewHolder(binding.root){

        fun getDate(timestamp: Any): String{
            val mFormat = "HH:mm"
            val sdf = SimpleDateFormat(mFormat, Locale.getDefault())
            return sdf.format(timestamp)
        }

        fun bind(item: Message){
            binding.message = item
            binding.itemMessageOtherTime.setText(getDate(item.time!!))
            item.send_user_image?.let { Constant.loadUserImage(it,binding.ivItemMessageOtherImage) }
            binding.executePendingBindings()
        }
    }

再過來就是要在onCreateViewHolder來回傳不同的viewHolder

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        if(viewType == MESSAGE_TYPE_RIGHT){
            return MyMessageViewHolder(MessageItemListMeBinding.inflate(LayoutInflater.from(parent.context)))
        }else{
            return OtherMessageViewHolder(MessageListItemOtherBinding.inflate(LayoutInflater.from(parent.context)))
        }
    }

然後綁定viewHolder

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val model = getItem(position)
        when(holder){
            is  MyMessageViewHolder ->{
                holder.bind(model)

            }
            is  OtherMessageViewHolder -> {
                holder.bind(model)

            }
        }
    }

回到ChatRoomFragment,來setAdapter

private fun setAdapter(){
        chatAdapter = ChatRoomAdapter()
        binding.rvChatRoom.adapter = chatAdapter
        val linerLayout = LinearLayoutManager(requireContext())
        //讓我們的list從尾開始生成    
        linerLayout.reverseLayout = true
        binding.rvChatRoom.layoutManager = linerLayout
    }

三、讀取數據

接下來我們會讀取數據,我們可以透過設定addValueEventListener來幫助我們當數據有更改的時候,我們就拿出所有數據

首先我們先去ChatViewModle新增



private val _messageList = MutableLiveData<List<Message>>()
val messageList: LiveData<List<Message>>
get() = _messageList




fun messageValueListener(fragment: ChatRoomFragment, currentUID: String, invitationUID: String){

        val ref = FirebaseDatabase.getInstance().reference.child(currentUID).child(invitationUID)
        ref.addValueEventListener(object : ValueEventListener{
            override fun onDataChange(snapshot: DataSnapshot) {
                val list = mutableListOf<Message>()
                for (i in snapshot.children){
                    val message = i.getValue(Message::class.java)
                    if (message != null) {
                        list.add(message)
            
                    }
                }
               _messageList.postValue(list.reversed())
                
            }

            override fun onCancelled(error: DatabaseError) {
                TODO("Not yet implemented")
            }
        })
    }

我們要加入我們當前使用者的userId,以及要聊天的user_id,因為我們的所有聊天內容,不管是我傳給別人,或是別人傳給我,都會同時儲存在A→B,B→A的資料。

而onDataChagne,它的DataSnapshot是會回傳所有的資料,而不是只有新的資料,所以我們要把它全部加入到livedata,並且顯示到recyclerview。

而如果只是想要觀察child的資料呢,如你想要某個新增的資料,而不是全部都回傳給你,addChildEventListener是你的好選擇,裡面可以override以下的funtion,你可以看是要觀察新增的還是刪除的..

ref.addChildEventListener(object : ChildEventListener{
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                val model = snapshot.getValue(Message::class.java)
                Timber.d("child model : $model")
            }

            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {

                val model = snapshot.getValue(Message::class.java)
                Timber.d("child model chagne: $model")
            }

            override fun onChildRemoved(snapshot: DataSnapshot) {
            }

            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
            }

            override fun onCancelled(error: DatabaseError) {
            }

        })

而因為我們這次是要丟入到livedata,所以我們要所有資料,所以我們就選擇addValueEventListener

再過來就是在Fragment呼叫它囉。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        chatViewModel.messageValueListener(this,accountViewModel.userDetail.value!!.id,matchingViewModel.selectedInvitation.value!!.user_id)
        super.onViewCreated(view, savedInstanceState)
    }

四、離線讀取

如果我們希望使用者在沒有網路狀態的時候,依舊可以看到聊天訊息呢? 總是怕如果再沒有網路的情況,竟然也忘記邀約的時間?

我們直接在 MyApp.kt的onCreated新增以下,就好啦!

Firebase.database.setPersistenceEnabled(true)

它會緩存聊天紀錄,而且也可以實現當你今天離線寫入的時候,別怕,當你恢復網路的時候就會自動幫你寫入~

五、修改Invitation的資料

我們希望我們的chatRoom上面顯示我們的聊天對象名稱,所以我們要把name放入Invitation

data class Invitation(
val id: String = "",
        val user_id: String = "",
				//新增這個
        val user_name: String? = "",
        val pet_image: String = "",
        val pet_type: String = "",
        val pet_type_description: String = "",
        val area: String = "",
        val date_place: String = "",
        val date_time: Timestamp? = null,
        val note: String = "",
        val update_time: Timestamp? = null ,

)

並且回到AddInvitationFragment的onClick裡面新增

user_name = accountViewModel.userDetail.value?.name,

最後回到ChatRoomFragment來新增以下,把它顯示出來就好啦

matchingViewModel.selectedInvitation.value?.user_name?.let {
           binding.tvChatRoomAcceptUserName.text = it
        }

day19.finish


上一篇
【day18】聊天室(上) X Realtime database
下一篇
【day20】創建對象列表(上)
系列文
30天建立寵物約散App-Android新手篇30

尚未有邦友留言

立即登入留言