如何实现离线时在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
相关产品推荐
相关产品推荐

