Notification 通知:从基础到渠道适配
背景
通知是 Android 系统最核心的交互入口之一。一条好的通知能在不打断用户当前操作的前提下传递关键信息------新消息来了、下载完成了、后台任务执行完毕了。反过来,滥用通知是用户卸载应用的头号理由。
从 Android 8.0(API 26)开始引入**通知渠道(Notification Channel)**的概念后,通知的管理权真正交到了用户手里:用户可以在系统设置里按渠道开关通知、调静默、改振动,而不需要开发者参与。这意味着如果适配不好,你的通知可能根本不会被展示。
本文从 NotificationCompat 出发,带你理解通知渠道体系,掌握常见的通知构建模式,并解决实际开发中的适配坑。
核心概念
NotificationChannel:通知的分类容器
Android 8.0 之后,所有通知必须关联到一个 NotificationChannel。渠道一旦创建,其重要性级别、声音、振动等属性就由用户接管,应用无法再修改。因此渠道的初始配置必须在首次创建时就设好。
渠道 ID 一旦确定,就不要随意更改,否则老用户的设置会丢失。
NotificationCompat.Builder:兼容构建器
始终使用 NotificationCompat.Builder(来自 androidx.core:core)构建通知,不要直接使用平台 API 的 Notification.Builder。Compat 层自动处理了从 API 16 到最新版本的各种兼容细节。
PendingIntent:延迟意图
点击通知后跳转到哪个 Activity、哪个 Service,都由 PendingIntent 决定。常见用法是 PendingIntent.getActivity() 配合 TaskStackBuilder 构建返回栈。
代码实战(Kotlin)
1. 创建通知渠道(必须在发通知前完成)
kotlin
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
object NotificationHelper {
const val CHANNEL_ID_MESSAGE = "channel_message"
const val CHANNEL_ID_TASK = "channel_task"
fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = context.getSystemService(NotificationManager::class.java)
// 消息渠道:高重要性,有声音和振动
val messageChannel = NotificationChannel(
CHANNEL_ID_MESSAGE,
"新消息",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "新消息通知,包括私信和评论回复"
enableVibration(true)
}
manager.createNotificationChannel(messageChannel)
// 后台任务渠道:低重要性,默认静默
val taskChannel = NotificationChannel(
CHANNEL_ID_TASK,
"任务进度",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "下载、同步等后台任务进度"
enableVibration(false)
}
manager.createNotificationChannel(taskChannel)
}
}
在 Application.onCreate() 中调用 NotificationHelper.createChannels(this)。
2. 发一条基础通知
kotlin
import android.app.PendingIntent
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
fun showBasicNotification(context: Context) {
val channelId = NotificationHelper.CHANNEL_ID_MESSAGE
// 点击后跳转
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info) // 必须设置
.setContentTitle("新消息")
.setContentText("你收到了一条新私信")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true) // 点击后自动消失
.build()
// notify id 不要用 0,用固定值保证同类通知可覆盖
NotificationManagerCompat.from(context).notify(1001, notification)
}
3. 大图/大文字样式
kotlin
// 大文字样式
val style = NotificationCompat.BigTextStyle()
.bigText("这是一段很长的消息正文,单行显示不下时,展开后可以看到完整内容。")
.setBigContentTitle("新消息(展开标题)")
.setSummaryText("来自张三")
// 或者大图样式
// val style = NotificationCompat.BigPictureStyle()
// .bigPicture(BitmapFactory.decodeResource(context.resources, R.drawable.demo))
// .setSummaryText("分享了一张图片")
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("新消息")
.setContentText("你收到了一条新私信")
.setStyle(style)
.build()
4. 带进度条的通知(下载/上传场景)
kotlin
fun showProgressNotification(context: Context, progress: Int, max: Int) {
val notification = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_ID_TASK)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("文件下载中")
.setContentText("已完成 ${progress * 100 / max}%")
.setProgress(max, progress, false) // false = 确定进度条
.setOngoing(true) // 不可滑动删除
.build()
NotificationManagerCompat.from(context).notify(2001, notification)
}
下载完成后更新通知:
kotlin
val doneNotification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("下载完成")
.setContentText("文件已保存到本地")
.setProgress(0, 0, false) // 清除进度条
.setOngoing(false)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context).notify(2001, doneNotification)
5. 通知分组
Android 7.0 起支持通知分组,同一组的通知会折叠成一束,展开后能看到全部。
kotlin
val groupKey = "group_chat_room_42"
// 每条通知都加 setGroup(groupKey)
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("张三")
.setContentText("你好!")
.setGroup(groupKey)
.build()
// 再发一条摘要通知作为组的头部
val summaryNotification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("会议室群")
.setContentText("3 条新消息")
.setGroup(groupKey)
.setGroupSummary(true)
.build()
避坑指南
SmallIcon 必须纯 alpha 通道。 系统通知栏的 SmallIcon 只取 Alpha 层忽略颜色。如果放一张彩色图,会显示成一个纯色方块,在各类 ROM 上效果千奇百怪。建议用矢量绘制纯白/纯透明图案。
Android 8.0 以下不创建渠道也能发通知。 这就是 Compat 的功劳------NotificationCompat.Builder(context, channelId) 在低版本上会忽略 channelId。但 Android 8.0+ 上如果没创建对应渠道,通知静默不会显示,连错误日志都没有,排查起来非常痛苦。
notify id 的管理。 调用 notify(id, notification) 时,相同 id 的通知会覆盖旧通知。这个特性很有用:进度条场景用相同 id 更新进度;不同消息用不同 id 让它们独立显示。但如果 id 管理混乱,会导致重要通知被意外覆盖。
PendingIntent 的 FLAG_IMMUTABLE。 从 Android 12 开始,创建 PendingIntent 必须显式指定 FLAG_IMMUTABLE 或 FLAG_MUTABLE,否则会直接崩溃。最佳实践是:能用 FLAG_IMMUTABLE 就用它,只有需要内部修改的场景才用 FLAG_MUTABLE。
前台服务通知不可关闭。 当通知关联到前台 Service 时(startForeground(id, notification)),用户无法滑动删除,这是系统级别保护。需要确保前台服务通知的渠道重要性不低于 IMPORTANCE_LOW,否则在 Android 8.0+ 上前台服务会直接崩溃。
不要滥用高重要性渠道。 IMPORTANCE_HIGH 的通知会响铃、振动、在屏幕上弹出 Heads-up。每天推十条以上的高优先级通知大概率会被用户关掉整个渠道,甚至卸载。后台任务进度、数据同步等场景一律用 IMPORTANCE_LOW。
总结
Notification 看似只是一条小横幅,背后却涉及渠道管理、版本兼容、生命周期绑定、用户意图跳转等一系列工程问题。核心原则就三条:
- 用 NotificationCompat 代替原生 API,省掉大部分兼容性问题
- 提前规划渠道体系,初始化时一次性建好,之后不要再改
- 尊重用户注意力,用什么级别、什么频率发通知,都要克制
掌握这些,你的应用通知就不会再被用户一刀切地关掉了。