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

Firebase Kotlin中获取ServerTimestamp生成的Date对象及消息排序问题

Firebase自动生成时间戳的获取与消息排序修复

问题场景

定义了消息数据类,通过@ServerTimestamp注解让Firebase自动生成消息时间戳,添加消息时将date设为null,但在监听获取消息时,要么无法将时间戳转为Date对象,要么出现空指针异常,导致无法正常显示时间和排序消息列表。

消息数据类

data class Message(
    var message: String,
    var id: String,
    var receiverID: String,
    @ServerTimestamp
    var date: Date? = null,
    var isChecked: Boolean,
    var type: String
)

添加消息代码

val message = Message(text, id, receiverUser.uid, null, false, MESSAGE_TYPE_TEXT)
database
    .collection(KEY_COLLECTION_USERS)
    .document(preferenceManager.getString(KEY_USER_ID)!!)
    .collection(KEY_COLLECTION_CHAT)
    .add(message)
mBinding.typeMessageField.setText(null)

原监听代码(存在问题)

database
    .collection(KEY_COLLECTION_USERS)
    .document(preferenceManager.getString(KEY_USER_ID)!!)
    .collection(KEY_COLLECTION_CHAT)
    .addSnapshotListener { value, error ->
        var messages_test = ArrayList(messages)
        if (error != null) {
            return@addSnapshotListener
        } else {
            if (value == null) {
                return@addSnapshotListener
            } else {
                for (documentChange in value.documentChanges) {
                    if (documentChange.type == DocumentChange.Type.ADDED) {
                        var doc = documentChange.document
                        if (doc.getString(KEY_MESSAGE_RECEIVER_ID) == receiverUser.uid) {
                            var text = doc.getString(KEY_MESSAGE).toString()
                            var id = doc.getString(KEY_MESSAGE_ID).toString()
                            var receiverId =
                                doc.getString(KEY_MESSAGE_RECEIVER_ID).toString()
                            var date = doc.getTimestamp(KEY_MESSAGE_DATE)?.toDate()

                            var isChecked = doc.getBoolean(KEY_MESSAGE_ISCHECKED)!!
                            var type = doc.getString(KEY_TYPE_MESSAGE).toString()
                            var message =
                                Message(text, id, receiverId, date, isChecked, type)
                            if (message !in messages) {
                                messages.add(message)
                            }
                        }
                    }
                }
                messages.sortBy { it -> it.date }
                var messages_test = ArrayList(messages)
                userChatAdpater.updateList(messages_test)
                if (messages.size != 0) {
                    mBinding.rvUserChat.smoothScrollToPosition(messages.size - 1)
                }
            }
        }
    }

问题分析

  1. 空指针风险:直接对getString()返回值调用toString(),或用!!强制解包布尔值,当字段不存在时会触发空指针。
  2. 字段映射不匹配:若KEY_MESSAGE_DATE与数据类中date字段的Firestore存储键名不一致,会导致获取到null时间戳。
  3. 排序逻辑漏洞:sortBy { it.date }未处理date为null的情况,可能导致排序异常。
  4. 低效去重:通过message !in messages判断重复,依赖data class的全字段比较,效率低且可能因date值差异误判。

修复方案

1. 安全获取字段值

改用安全调用+默认值,避免空指针:

val text = doc.getString(KEY_MESSAGE) ?: ""
val id = doc.getString(KEY_MESSAGE_ID) ?: ""
val receiverId = doc.getString(KEY_MESSAGE_RECEIVER_ID) ?: ""
val date = doc.getTimestamp(KEY_MESSAGE_DATE)?.toDate()
val isChecked = doc.getBoolean(KEY_MESSAGE_ISCHECKED) ?: false
val type = doc.getString(KEY_TYPE_MESSAGE) ?: MESSAGE_TYPE_TEXT

2. 统一字段映射

在数据类中明确指定Firestore字段名,避免驼峰转蛇形的自动映射误差:

data class Message(
    var message: String,
    var id: String,
    var receiverID: String,
    @ServerTimestamp
    @FieldName(KEY_MESSAGE_DATE) // 明确指定数据库字段名
    var date: Date? = null,
    var isChecked: Boolean,
    var type: String
)

3. 安全排序

处理date为null的极端情况,将这类消息排在末尾:

messages.sortBy { it.date ?: Date(0) }

4. 高效去重

通过消息唯一ID判断重复,比全字段比较更可靠高效:

val existingMessage = messages.find { it.id == message.id }
if (existingMessage == null) {
    messages.add(message)
}

修复后的完整监听代码

database
    .collection(KEY_COLLECTION_USERS)
    .document(preferenceManager.getString(KEY_USER_ID)!!)
    .collection(KEY_COLLECTION_CHAT)
    .addSnapshotListener { value, error ->
        if (error != null || value == null) {
            return@addSnapshotListener
        }

        for (documentChange in value.documentChanges) {
            if (documentChange.type == DocumentChange.Type.ADDED) {
                val doc = documentChange.document
                if (doc.getString(KEY_MESSAGE_RECEIVER_ID) == receiverUser.uid) {
                    val text = doc.getString(KEY_MESSAGE) ?: ""
                    val id = doc.getString(KEY_MESSAGE_ID) ?: ""
                    val receiverId = doc.getString(KEY_MESSAGE_RECEIVER_ID) ?: ""
                    val date = doc.getTimestamp(KEY_MESSAGE_DATE)?.toDate()
                    val isChecked = doc.getBoolean(KEY_MESSAGE_ISCHECKED) ?: false
                    val type = doc.getString(KEY_TYPE_MESSAGE) ?: MESSAGE_TYPE_TEXT

                    val message = Message(text, id, receiverId, date, isChecked, type)
                    val existingMessage = messages.find { it.id == message.id }
                    if (existingMessage == null) {
                        messages.add(message)
                    }
                }
            }
        }

        messages.sortBy { it.date ?: Date(0) }
        userChatAdpater.updateList(ArrayList(messages))
        if (messages.isNotEmpty()) {
            mBinding.rvUserChat.smoothScrollToPosition(messages.size - 1)
        }
    }

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 12:36:32