【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


<<:  D19 - 今晚我想来点 唯独派 getter 唯写派 setter

>>:  Day21_控制项(A16资讯安全事故管理)

第9章:操作目录与档案权限介绍

前言 在上一章节中,我们介绍了基本使用者管理,在这一章节中,介绍档案之目录与档案系统权限。 Linu...

第46天-fd-find 代替 find

使用 fd-find 代替 find,效率更好 下载方式 : sudo apt-get instal...

Day-29 了解 Kubernetes AutoScaler

前言 到了这个章节大家可能会开始回想,刚开始听到K8S时很多人都说Kubernetes的AutoSc...

D9-用 Swift 和公开资讯,打造投资理财的 Apps { 台股申购实作.2 -读取Big5码的csv}

现在开发者写程序,最方便的一点,就是不会的地方,可以问 Google 在 Google 中输入 Sw...

[DAY 19] 卡多利亚良食故事馆

卡多利亚良食故事馆 地点:台南市後壁区42-27号 时间:9:00~17:00 对於一个研替来说 最...