You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现离线时在RecyclerView中即时显示发送方聊天消息(仿WhatsApp)

实现发送方即时离线查看聊天消息(WhatsApp风格)

核心逻辑是发送消息时先本地插入临时消息,无需等待Firestore回调,待云端操作完成后再更新消息状态,具体实现步骤如下:


1. 扩展数据类,增加消息状态字段

修改SingleChatData,新增状态字段标记消息处于发送中、已发送还是发送失败:

class SingleChatData{
    var senderId: String?= null
    var message: String?= null
    var messageId: String?= null
    var time: String?= null
    var chatDay: String?= null
    var fullDate: String?= null
    var type: String?= null
    var selected: Boolean = false
    var msgId2: String?= null
    // 新增状态字段:pending(发送中)、sent(已发送)、failed(发送失败)
    var status: String = "pending"

    constructor(message: String?, senderId: String?, messageId: String?,time: String, chatDay: String?,
                fullDate: String?, type: String?, status: String = "pending"){
        this.message = message
        this.senderId = senderId
        this.messageId = messageId
        this.time = time
        this.chatDay = chatDay
        this.fullDate = fullDate
        this.type = type
        this.status = status
    }
}

2. 发送消息时先插入本地临时消息

修改图片发送的点击事件,在触发Firestore上传前,先创建临时消息插入列表,让用户即时看到:

imgSend.setOnClickListener {
    if (mSaveImageUri != null) {
        // --- 第一步:本地插入临时消息 ---
        val date = Date()
        val calendar = Calendar.getInstance()
        val day = calendar.get(Calendar.DAY_OF_WEEK)
        var currentDay = ""
        when (day) {
            Calendar.SUNDAY -> currentDay = "Sunday"
            Calendar.MONDAY -> currentDay = "Monday"
            Calendar.TUESDAY -> currentDay = "Tuesday"
            Calendar.WEDNESDAY -> currentDay = "Wednesday"
            Calendar.THURSDAY -> currentDay = "Thursday"
            Calendar.FRIDAY -> currentDay = "Friday"
            Calendar.SATURDAY -> currentDay = "Saturday"
        }
        // 生成临时ID(后续用Firestore真实ID替换)
        val tempMsgId = "temp_${System.currentTimeMillis()}"
        // 格式化时间显示
        val ss = SimpleDateFormat("hh:mm a", Locale.getDefault())
        val msgTime = ss.format(date).toString()
        val sdf = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault())
        val fullDate = sdf.format(date).toString()
        // 创建临时图片消息(用本地Uri作为内容,标记为发送中)
        val tempImageMsg = SingleChatData(
            message = mSaveImageUri.toString(),
            senderId = senderUid,
            messageId = tempMsgId,
            time = msgTime,
            chatDay = currentDay,
            fullDate = fullDate,
            type = "photo",
            status = "pending"
        )
        // 插入列表并通知适配器更新
        messages.add(tempImageMsg)
        adapter.notifyItemInserted(messages.size - 1)
        // 自动滚动到底部
        recyclerView.post {
            recyclerView.smoothScrollToPosition(messages.size - 1)
        }

        // --- 第二步:执行Firestore上传和存储操作 ---
        val reference = storage.reference.child("Chats").child(senderRoom)
            .child("Image Messages").child(senderUid)
        val reference2 = storage.reference.child("Chats").child(receiverRoom)
            .child("Image Messages").child(senderUid)
        reference.putFile(mSaveImageUri!!).addOnCompleteListener { task ->
            reference2.putFile(mSaveImageUri!!)
            if (task.isSuccessful) {
                reference.downloadUrl.addOnSuccessListener { uri ->
                    val imgMsg = uri.toString()
                    val id = firestore.collection("Chats").document().id
                    val imageMsg = hashMapOf(
                        "message" to imgMsg,
                        "senderId" to senderUid,
                        "date" to calendar,
                        "time" to date,
                        "day" to currentDay,
                        "seen" to "false",
                        "type" to "photo"
                    )
                    firestore.collection("Chats").document(senderRoom)
                        .collection("messages").document(id)
                        .set(imageMsg).addOnSuccessListener {
                            firestore.collection("Chats").document(receiverRoom)
                                .collection("messages").document(id)
                                .set(imageMsg).addOnSuccessListener {
                                    // --- 第三步:更新本地消息状态和内容 ---
                                    val tempMsgIndex = messages.indexOfFirst { it.messageId == tempMsgId }
                                    if (tempMsgIndex != -1) {
                                        val updatedMsg = messages[tempMsgIndex].apply {
                                            this.message = imgMsg
                                            this.messageId = id
                                            this.status = "sent"
                                        }
                                        messages[tempMsgIndex] = updatedMsg
                                        adapter.notifyItemChanged(tempMsgIndex)
                                    }
                                }
                                .addOnFailureListener {
                                    // 发送失败,更新状态为failed
                                    val tempMsgIndex = messages.indexOfFirst { it.messageId == tempMsgId }
                                    if (tempMsgIndex != -1) {
                                        val updatedMsg = messages[tempMsgIndex].apply {
                                            this.status = "failed"
                                        }
                                        messages[tempMsgIndex] = updatedMsg
                                        adapter.notifyItemChanged(tempMsgIndex)
                                        Toast.makeText(applicationContext, "Failed", Toast.LENGTH_SHORT).show()
                                    }
                                }
                        }
                }
            } else {
                // 上传失败,更新状态为failed
                val tempMsgIndex = messages.indexOfFirst { it.messageId == tempMsgId }
                if (tempMsgIndex != -1) {
                    val updatedMsg = messages[tempMsgIndex].apply {
                        this.status = "failed"
                    }
                    messages[tempMsgIndex] = updatedMsg
                    adapter.notifyItemChanged(tempMsgIndex)
                    Toast.makeText(applicationContext, "Upload Failed", Toast.LENGTH_SHORT).show()
                }
            }
        }
    } else {
        Toast.makeText(applicationContext, "Please save your image first", Toast.LENGTH_SHORT).show()
    }
}

3. 修改Firestore快照监听,避免清空本地消息

原setDataToAdapter会清空整个消息列表,导致本地临时消息被覆盖,改为增量更新仅同步云端新增/修改的消息:

private fun setDataToAdapter() {
    firestore.collection("Chats").document(senderRoom!!).collection("messages").orderBy("time")
        .addSnapshotListener { value, error ->
            if (error != null) return@addSnapshotListener
            value?.documentChanges?.forEach { change ->
                val document = change.document
                val coMsg = document.data?.get("message").toString()
                val coId = document.data?.get("senderId").toString()
                val msgId = document.id
                // 格式化时间和日期
                val calendarMap = document.data?.get("date") as Map<*, *>
                val curTimestamp = calendarMap["time"] as com.google.firebase.Timestamp
                val millis = curTimestamp.seconds * 1000 + curTimestamp.nanoseconds / 1000000
                val ss = SimpleDateFormat("hh:mm a", Locale.getDefault())
                val msgTime = ss.format(Date(millis)).toString()
                val chatDay = document.data?.get("day").toString()
                val fullDateStamp = calendarMap["time"] as com.google.firebase.Timestamp
                val dateInMillis = fullDateStamp.seconds * 1000 + fullDateStamp.nanoseconds / 1000
                val sdf = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault())
                val fullDate = sdf.format(Date(dateInMillis)).toString()
                val type = document.data?.get("type").toString()

                when (change.type) {
                    // 新增消息:仅当本地无该消息时添加
                    com.google.firebase.firestore.DocumentChange.Type.ADDED -> {
                        val existingMsg = messages.find { it.messageId == msgId }
                        if (existingMsg == null) {
                            val coMsgT = SingleChatData(coMsg, coId, msgId, msgTime, chatDay, fullDate, type, status = "sent")
                            messages.add(coMsgT)
                            adapter.notifyItemInserted(messages.size - 1)
                            // 首次加载时滚动到底部
                            if (!firstScrollDone) {
                                recyclerView.post {
                                    recyclerView.smoothScrollToPosition(adapter.itemCount - 1)
                                }
                                firstScrollDone = true
                            }
                        }
                    }
                    // 修改消息:同步已读状态等字段
                    com.google.firebase.firestore.DocumentChange.Type.MODIFIED -> {
                        val msgIndex = messages.indexOfFirst { it.messageId == msgId }
                        if (msgIndex != -1) {
                            val updatedMsg = messages[msgIndex].apply {
                                // 可在此同步已读状态:this.seen = document.data?.get("seen").toString()
                            }
                            messages[msgIndex] = updatedMsg
                            adapter.notifyItemChanged(msgIndex)
                        }
                    }
                    // 删除消息:移除对应条目
                    com.google.firebase.firestore.DocumentChange.Type.REMOVED -> {
                        val msgIndex = messages.indexOfFirst { it.messageId == msgId }
                        if (msgIndex != -1) {
                            messages.removeAt(msgIndex)
                            adapter.notifyItemRemoved(msgIndex)
                        }
                    }
                }
            }
        }
}

4. 修改Adapter,显示消息状态

在SentMsgViewHolder分支中,根据status字段显示对应的发送状态(比如加载动画、已发送对勾、失败重试图标):

// 在SentMsgViewHolder的绑定逻辑中添加状态显示
when (currentMsg.status) {
    "pending" -> {
        // 显示发送中动画(示例:ProgressBar或加载图标)
        viewHolder.binding.sentStatus.visibility = View.VISIBLE
        viewHolder.binding.sentStatus.setImageResource(R.drawable.ic_pending)
    }
    "sent" -> {
        // 显示已发送对勾
        viewHolder.binding.sentStatus.visibility = View.VISIBLE
        viewHolder.binding.sentStatus.setImageResource(R.drawable.ic_sent)
    }
    "failed" -> {
        // 显示发送失败图标,可添加重试点击事件
        viewHolder.binding.sentStatus.visibility = View.VISIBLE
        viewHolder.binding.sentStatus.setImageResource(R.drawable.ic_failed)
        viewHolder.binding.sentStatus.setOnClickListener {
            // 此处添加重试发送逻辑
        }
    }
}

内容的提问来源于stack exchange,提问作者Mahmoud Nabil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 22:09:57