Android FCM推送及通知栏展示
需求:
实现FIrebase Cloud Message推送功能,用户收到通知后,可以悬浮通知,自定义的大/小通知展示在通知栏,判断前台/后台,点击后进行跳转。
步骤:
一、配置及接入依赖库
1.下载 google-services.json
并放入 app/
目录
2.项目里:
dependencies {
classpath 'com.google.gms:google-services:4.4.0' // 确保使用最新版本
}
3.app的build.gradle
plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
}
dependencies {
implementation 'com.google.firebase:firebase-messaging:23.2.1' // 最新版本
}
tips:Android 13及以后,必须动态申请通知权限哦,不然不给展示
二、FirebaseMessagingService实现接收通知
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.e("FCM", "New Token:$token")
BaseApp.pushId = token
UserDataUtils.toUpdate(token)//上传给服务器
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
LogUtils.e("FCM", "remoteMessage: $remoteMessage")
remoteMessage.notification?.let {
Log.e("FCM", "Message Notification Title: ${it.title}")
Log.e("FCM", "Message Notification Body: ${it.body}")
}
remoteMessage.data.let {
Log.e("FCM", "Message Data Payload: $it")
}
sendNotification2(remoteMessage.data)//我这里使用的是data里的数据
}
private fun sendNotification2(data: MutableMap<String, String>) {
val channelId = "default_channel" // 通知通道 ID
val channelName = "General Notifications" // 通知通道名称
// 判断目标 Activity
val targetActivity: Class<*> = if (isAppAlive(this, packageName)) {
LogUtils.e("FCM", "isAppAlive ${isAppAlive(this, packageName)} isBackGround ${BaseApp.isBackGround}")
if (BaseApp.isBackGround) SplashAc::class.java else PlayDetailAc::class.java
} else {
SplashAc::class.java
}
val shortData = data["payload"]
val gson = Gson()
val shortPlay = gson.fromJson(shortData, ShortPlay::class.java)
// 跳转逻辑
val intent = Intent(this, targetActivity).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
if (targetActivity == PlayDetailAc::class.java) {
putExtra(EXTRA_SHORT_PLAY, shortPlay)
} else {
putExtra(AppConstants.SHORTPLAY_ID, shortPlay) // 冷启动时传递自定义数据
}
}
val pendingIntent = PendingIntent.getActivity(
this, System.currentTimeMillis().toInt(), intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 获取通知管理器
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// 创建通知通道(适配 Android 8.0+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply {
description = "This channel is used for general notifications"
enableLights(true)
lightColor = Color.BLUE
enableVibration(true)
}
notificationManager.createNotificationChannel(channel)
}
val radiusPx = (12 * resources.displayMetrics.density).toInt()
val requestOptions = RequestOptions()
.transform(CenterCrop(), RoundedCorners(radiusPx))
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.placeholder(R.mipmap.card_default)
.error(R.mipmap.card_default)
val customView = RemoteViews(packageName, R.layout.custom_notification_layout).apply {
setTextViewText(R.id.notification_title, shortPlay.title ?: "Chill Shorts")
setTextViewText(R.id.notification_message, shortPlay.desc ?: "Chill Shorts")
}
val smallCustomView = RemoteViews(packageName, R.layout.custom_notification_small_layout).apply {
setTextViewText(R.id.notification_title, shortPlay.title ?: "Chill Shorts")
setTextViewText(R.id.notification_message, shortPlay.desc ?: "Chill Shorts")
}
// 使用 Glide 加载图片并构建通知
Glide.with(this).asBitmap().apply(requestOptions).load(shortPlay.coverImage).into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
// 设置图片到自定义视图
customView.setImageViewBitmap(R.id.notification_icon, resource)
smallCustomView.setImageViewBitmap(R.id.notification_icon, resource)
// 构建通知
val notificationBuilder = NotificationCompat.Builder(this@MyFirebaseMessagingService, channelId)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomHeadsUpContentView(smallCustomView)
.setSmallIcon(R.mipmap.app_logo_round)
.setCustomContentView(smallCustomView)
.setCustomBigContentView(customView)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
val notification = notificationBuilder.build()
notification.flags = Notification.FLAG_AUTO_CANCEL
// 发送通知
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
// 图片加载失败,使用默认占位图
customView.setImageViewResource(R.id.notification_icon, R.mipmap.card_default)
smallCustomView.setImageViewResource(R.id.notification_icon, R.mipmap.card_default)
// 构建通知
val notificationBuilder = NotificationCompat.Builder(this@MyFirebaseMessagingService, channelId)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomHeadsUpContentView(smallCustomView)
.setSmallIcon(R.mipmap.app_logo_round)
.setCustomContentView(smallCustomView)
.setCustomBigContentView(customView)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
// 发送通知
notificationManager.notify(System.currentTimeMillis().toInt(), notificationBuilder.build())
}
override fun onLoadCleared(placeholder: Drawable?) {
// 清理资源时无操作
}
})
}
private fun isAppAlive(context: Context, packageName: String): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appProcesses = activityManager.runningAppProcesses
appProcesses?.forEach {
if (it.processName == packageName && it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true
}
}
return false
}
}
三、注意事项和代码逻辑
1.RemoteMessage.data获取控制台的配置的数据
2.isAppAlive判断当前App是否存活,isBackGround判断App是否处于后台
3.intent和pendingIntent要设置正确的,合适的flag
常见的 PendingIntent Flags
Flag | 说明 |
---|---|
FLAG_CANCEL_CURRENT | 取消当前已有的 PendingIntent ,并创建新的 PendingIntent (适用于要确保新的 Intent 被处理的情况)。 |
FLAG_UPDATE_CURRENT | 更新已存在的 PendingIntent ,保持 Intent 的 Extras 最新。 |
FLAG_NO_CREATE | 如果 PendingIntent 存在,则返回它;否则返回 null ,不会创建新的 PendingIntent 。 |
FLAG_ONE_SHOT | PendingIntent 只能使用一次,执行后自动销毁。 |
FLAG_IMMUTABLE (API 23+) | PendingIntent 不能被修改(Android 12 及以上必须显式指定 FLAG_IMMUTABLE 或 FLAG_MUTABLE )。 |
FLAG_MUTABLE (API 31+) | PendingIntent 可以被 AlarmManager 、NotificationManager 等修改,适用于 Foreground Service 及 Remote Input。 |
4.常见的 Notification Flags
Flag | 说明 |
---|---|
Notification.FLAG_AUTO_CANCEL | 点击通知后自动取消(移除通知) |
Notification.FLAG_ONGOING_EVENT | 使通知成为前台通知(用户不能手动清除) |
Notification.FLAG_NO_CLEAR | 不能通过滑动或清除按钮删除通知 |
Notification.FLAG_FOREGROUND_SERVICE | 适用于前台服务的通知 |
Notification.FLAG_INSISTENT | 让通知的声音、震动等一直持续,直到用户处理 |
5. 记得适配NotificationChannel。
6.RemoteViews是用于设置通知的自定义View的,在上述的代码里,我设置了
val notificationBuilder = NotificationCompat.Builder(this@MyFirebaseMessagingService, channelId) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomHeadsUpContentView(smallCustomView)//悬浮通知 .setSmallIcon(R.mipmap.app_logo_round) .setCustomContentView(smallCustomView)//正常的自定义通知view .setCustomBigContentView(customView)//展开后的大通知View .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(pendingIntent) .setAutoCancel(true) val notification = notificationBuilder.build() notification.flags = Notification.FLAG_AUTO_CANCEL // 发送通知 notificationManager.notify(System.currentTimeMillis().toInt(), notification)
7.在 Android 8.0 (API 26) 及更高版本,官方建议使用 NotificationChannel
控制通知行为,而不是直接使用 flags
。使用.setAutoCancel(true) // 等价于 FLAG_AUTO_CANCEL,但是上面为啥我加了notification.flags = Notification.FLAG_AUTO_CANCEL,是因为设置自定义的通知,似的setAutoCancel失效了,所以又对flag进行了配置。(这个坑了我好一会儿)
四、注册 FirebaseMessagingService
manifest.xml
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
五、获取Token
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
LogUtils.e("FCM Token", "token:${task.result}")
pushId = task.result // 这是你的 Firebase Push ID
UserDataUtils.toUpdate(pushId.toString())//上传后端
} else {
LogUtils.e("FCM Token", "获取 Firebase Push ID 失败")
}
}
📌 案例:IM 消息推送通知的设计与优化
常见问题及其优化:
🟢 问题 1:消息推送通知丢失
🛠 问题描述
- IM 消息是通过 FCM(Firebase Cloud Messaging) 或 厂商推送(小米、华为、OPPO) 发送的,有时候通知收不到,比如:
- App 进程被杀死,无法接收到推送。
- Android 8.0+ 以后,应用后台时间过长,FCM 推送可能被系统限制。
- 部分国产 ROM 对后台进程的管控严格,通知会被系统拦截。
🚀 解决方案
- FCM 作为主要推送通道(Google Play 设备)。
- 厂商通道(小米、华为、OPPO、vivo)作为备用推送通道。
- 长连接保活(用户在线时,直接使用 WebSocket 推送)。
-
检测推送通道是否可用:
- 如果 FCM 无法收到消息,就尝试 WebSocket 或 轮询 获取未读消息。
-
使用 WorkManager 确保消息送达:
- 在
onMessageReceived()
里保存消息,防止推送丢失。 - 结合
WorkManager
定时检查未读消息。
- 在
🟢 问题 2:重复通知、通知不合并
🛠 问题描述
- 多条 IM 消息推送后,每条消息都会弹出 独立通知,导致通知栏很混乱。
- 例如:
- 5 条新消息,出现 5 个通知。
- 点击某个通知后,其他通知还在。
🚀 解决方案
-
使用
setGroup()
进行通知分组- 单聊消息:不同用户的聊天,不同 ID(
notify(userID, notification)
)。 - 群聊消息:同一个群的消息,使用
setGroup()
归类。val groupKey = "IM_GROUP_CHAT" // 子通知 val messageNotification = NotificationCompat.Builder(this, channelId) .setContentTitle("新消息") .setContentText("你有 3 条未读消息") .setSmallIcon(R.drawable.ic_message) .setGroup(groupKey) .build() // 汇总通知(id = 0,保证只有一个) val summaryNotification = NotificationCompat.Builder(this, channelId) .setContentTitle("IM 消息") .setContentText("你有新的消息") .setSmallIcon(R.drawable.ic_message) .setGroup(groupKey) .setGroupSummary(true) .build() notificationManager.notify(1, messageNotification) notificationManager.notify(0, summaryNotification)
- 单聊消息:不同用户的聊天,不同 ID(
🟢 问题 3:通知点击后跳转异常
🛠 问题描述
- 用户点击通知后,应该跳转到 聊天页面,但可能会:
- 进入应用后,未能正确跳转到聊天界面。
- 如果 App 进程被杀死,点击通知后只能进入启动页,而不是聊天页面。
🚀 解决方案
-
使用
PendingIntent.FLAG_UPDATE_CURRENT
确保 intent 只创建一次 -
App 被杀死时,恢复正确页面,
- 在
SplashActivity
里判断Intent
,决定是否直接进入聊天界面:if (intent?.hasExtra("chatId") == true) { startActivity(Intent(this, ChatActivity::class.java).apply { putExtras(intent.extras!!) }) finish() }