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) {

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() {

override fun onResourceReady(resource: Bitmap, transition: Transition?) {

// 设置图片到自定义视图

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_SHOTPendingIntent 只能使用一次,执行后自动销毁。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

android:name=".MyFirebaseMessagingService"

android:exported="false">

五、获取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)

🟢 问题 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()

}

Copyright © 2022 中国足球世界杯_90年世界杯 - doulol.com All Rights Reserved.