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

如何为聊天应用实现Firebase通知的离线存储功能?

FCM聊天应用离线消息缓存实现指导

核心问题分析

你遇到的SharedPreferences存储失败,大概率是因为FCM消息类型的处理逻辑错误:

  • 仅含notification字段的通知消息,在应用后台/关闭状态下会由Android系统直接处理,不会触发MyFirebaseMessagingService的onMessageReceived方法,自然无法执行存储逻辑。
  • SharedPreferences仅适合存储少量键值对,多条消息存入会覆盖,且无结构化存储能力,不适合聊天场景。

正确实现步骤

1. 调整FCM消息发送格式

要求后端发送数据消息(仅包含data字段),或混合消息(同时包含notification和data字段)。只有data字段的内容会在任何状态下(前台/后台/应用关闭)传递到onMessageReceived。

2. 用Room数据库替代SharedPreferences

Room是Android官方推荐的本地持久化方案,支持结构化存储、查询、排序,完全适配聊天消息的存储需求。

示例Room实体类与Dao:

// ChatMessage实体类
@Entity(tableName = "chat_messages")
data class ChatMessage(
    @PrimaryKey val messageId: String,
    val senderId: String,
    val content: String,
    val timestamp: Long,
    val isReceived: Boolean
)

// ChatDao接口
@Dao
interface ChatDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertMessage(message: ChatMessage)
    
    @Query("SELECT * FROM chat_messages ORDER BY timestamp DESC")
    fun getAllMessages(): Flow<List<ChatMessage>>
}

// 数据库实例
@Database(entities = [ChatMessage::class], version = 1)
abstract class ChatDatabase : RoomDatabase() {
    abstract fun chatDao(): ChatDao
    
    companion object {
        @Volatile
        private var INSTANCE: ChatDatabase? = null
        
        fun getInstance(context: Context): ChatDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    ChatDatabase::class.java,
                    "chat_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

3. 修改MyFirebaseMessagingService实现

在onMessageReceived中解析data字段,将消息存储到Room数据库,并处理通知显示:

class MyFirebaseMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        
        // 优先解析data字段内容
        remoteMessage.data.takeIf { it.isNotEmpty() }?.let { data ->
            val messageId = data["message_id"] ?: UUID.randomUUID().toString()
            val senderId = data["sender_id"].orEmpty()
            val content = data["content"].orEmpty()
            val timestamp = data["timestamp"]?.toLong() ?: System.currentTimeMillis()
            
            // 存储到Room数据库(用协程避免阻塞主线程)
            CoroutineScope(Dispatchers.IO).launch {
                ChatDatabase.getInstance(applicationContext).chatDao().insertMessage(
                    ChatMessage(messageId, senderId, content, timestamp, isReceived = true)
                )
            }
            
            // 根据应用状态处理通知
            if (isAppForeground()) {
                // 前台状态可更新UI或弹通知
                showInAppNotification(content)
            } else {
                // 后台/关闭状态,自行构建通知
                buildSystemNotification(content, senderId)
            }
        }
    }
    
    // 判断应用是否在前台
    private fun isAppForeground(): Boolean {
        val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        return activityManager.runningAppProcesses?.any {
            it.processName == packageName && it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
        } ?: false
    }
    
    // 构建系统通知
    private fun buildSystemNotification(content: String, senderId: String) {
        val launchIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent = PendingIntent.getActivity(
            this, 0, launchIntent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )
        
        val notificationBuilder = NotificationCompat.Builder(this, "CHAT_CHANNEL")
            .setSmallIcon(R.drawable.ic_chat_notification)
            .setContentTitle("来自$senderId 的消息")
            .setContentText(content)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
        
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        // Android 8.0+ 创建通知渠道
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "CHAT_CHANNEL", "聊天消息",
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationManager.createNotificationChannel(channel)
        }
        notificationManager.notify(Random().nextInt(), notificationBuilder.build())
    }
    
    private fun showInAppNotification(content: String) {
        // 前台应用内显示通知逻辑,比如更新消息列表或弹Toast
        Toast.makeText(this, "新消息:$content", Toast.LENGTH_SHORT).show()
    }
}

4. 检查AndroidManifest配置

确保FCM服务注册正确:

<service
    android:name=".MyFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

<!-- Android 12+ 需添加POST_NOTIFICATIONS权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

关键注意事项

  • FCM会自动缓存离线消息,当设备重新联网时,会将缓存的消息推送给应用,无需额外处理离线消息的拉取。
  • 应用关闭状态下,FCM的系统托管服务会接收消息并触发onMessageReceived,只要你的服务注册正确,就能完成存储。
  • 避免在onMessageReceived中执行耗时操作,必须用协程或异步线程处理数据库存储。

参考内容翻译(原Stack Exchange核心观点)

当应用处于后台时,FCM的纯通知消息(仅含notification字段)会由Android系统直接展示,不会传递到onMessageReceived方法;只有数据消息(仅含data字段)会在任何应用状态下触发onMessageReceived。如果需要在后台处理通知内容,应发送数据消息,然后在onMessageReceived中自行构建并显示通知,同时完成消息存储。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 04:36:10