第 15 章JAVA40 分钟
通知 Notification
掌握通知渠道、基本通知构建、丰富样式(大图/大文本/收件箱)、PendingIntent 点击跳转、进度通知、前台服务通知与 Android 13+ 通知权限
学习目标
- 理解 Android 8+ 必需的 NotificationChannel 与各配置项作用
- 用 NotificationCompat 构建基本通知并发布到指定渠道
- 实现大图、大文本、收件箱、消息样式通知
- 通过 PendingIntent 实现点击跳转与 Action 按钮
- 实现进度通知与前台服务常驻通知
- 在 Android 13+ 申请 POST_NOTIFICATIONS 权限
学习目标
通知是 Android 与用户“非实时屏幕”沟通的核心渠道,无论是消息推送、下载进度、还是前台服务常驻都离不开它。本章目标:
- 理解 Android 8.0+ 强制要求的 NotificationChannel;
- 用
NotificationCompat构建基本通知并发布; - 应用 BigTextStyle / BigPictureStyle / InboxStyle / MessagingStyle 等样式;
- 用
PendingIntent实现点击跳转、Action 按钮; - 实现进度条通知、前台服务常驻通知;
- 在 Android 13+ 申请
POST_NOTIFICATIONS权限。
通知渠道 NotificationChannel
1. 为什么必须有渠道
从 Android 8.0(API 26)起,所有通知必须归属某个渠道。若不创建渠道直接发通知,系统会静默丢弃且不报错。渠道一旦创建就不能修改 importance,但可在系统设置页由用户调整。
2. 创建渠道
public class NotificationHelper {
public static final String CHANNEL_GENERAL = "channel_general";
public static final String CHANNEL_DOWNLOAD = "channel_download";
public static final String CHANNEL_MESSAGE = "channel_message";
public static final String CHANNEL_FOREGROUND = "channel_foreground";
public static void createChannels(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager nm =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel general = new NotificationChannel(
CHANNEL_GENERAL, "常规通知",
NotificationManager.IMPORTANCE_DEFAULT);
general.setDescription("普通提醒");
general.enableVibration(true);
general.setShowBadge(true);
NotificationChannel download = new NotificationChannel(
CHANNEL_DOWNLOAD, "下载进度",
NotificationManager.IMPORTANCE_LOW);
download.setDescription("低优先级下载进度");
download.setSound(null, null);
NotificationChannel message = new NotificationChannel(
CHANNEL_MESSAGE, "即时消息",
NotificationManager.IMPORTANCE_HIGH);
message.setDescription("高优先级消息,带声音");
message.enableLights(true);
message.setLightColor(Color.RED);
NotificationChannel foreground = new NotificationChannel(
CHANNEL_FOREGROUND, "前台服务",
NotificationManager.IMPORTANCE_LOW);
foreground.setDescription("正在运行的后台服务");
foreground.setShowBadge(false);
nm.createNotificationChannels(Arrays.asList(general, download, message, foreground));
}
}
}
3. 在 Application 中初始化
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
NotificationHelper.createChannels(this);
}
}
基本通知构建
1. Gradle 依赖
dependencies {
implementation 'androidx.core:core:1.13.1'
}
2. 基本通知
public class MainActivity extends AppCompatActivity {
private static final int NOTIFY_ID_BASIC = 1001;
private void sendBasicNotification() {
Intent intent = new Intent(this, DetailActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this,
NotificationHelper.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_notification) // 必填,单色透明 PNG
.setContentTitle("标题")
.setContentText("这是一条普通通知")
.setAutoCancel(true) // 点击后自动消失
.setContentIntent(pi)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build();
NotificationManagerCompat.from(this).notify(NOTIFY_ID_BASIC, notification);
}
}
必须传
FLAG_IMMUTABLE或FLAG_MUTABLE(Android 12+),否则抛IllegalArgumentException。无内部修改需求时优先用FLAG_IMMUTABLE,更安全。
丰富样式
1. BigTextStyle
Notification noti = new NotificationCompat.Builder(this, CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("文章更新")
.setStyle(new NotificationCompat.BigTextStyle()
.bigText("这是一段超长正文内容,通知展开时会完整显示,"
+ "默认折叠状态下只显示 setContentText 的内容。"
+ "继续展开即可看到完整正文……"))
.build();
2. BigPictureStyle
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.banner);
Notification noti = new NotificationCompat.Builder(this, CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("新封面发布")
.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(bitmap)
.bigLargeIcon(null))
.build();
3. InboxStyle
Notification noti = new NotificationCompat.Builder(this, CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("3 条未读消息")
.setStyle(new NotificationCompat.InboxStyle()
.addLine("Alice:明天一起吃饭吗?")
.addLine("Bob:项目文档已更新")
.addLine("Carol:下班路上小心")
.setSummaryText("3 条未读"))
.build();
4. MessagingStyle(适合 IM)
Notification noti = new NotificationCompat.Builder(this, CHANNEL_MESSAGE)
.setSmallIcon(R.drawable.ic_notification)
.setStyle(new NotificationCompat.MessagingStyle("我")
.addMessage(new NotificationCompat.MessagingStyle.Message(
"你今天有空吗?", System.currentTimeMillis(), "Alice"))
.addMessage(new NotificationCompat.MessagingStyle.Message(
"项目有点问题想和你聊聊", System.currentTimeMillis() + 1000, "Alice")))
.build();
点击跳转与 Action 按钮
1. 点击 PendingIntent 跳转
private PendingIntent buildContentIntent() {
Intent intent = new Intent(this, MessageActivity.class);
intent.putExtra("extra_message_id", "msg_001");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
}
2. Action 按钮
Intent replyIntent = new Intent(this, ReplyActionReceiver.class);
replyIntent.setAction("ACTION_REPLY");
replyIntent.putExtra("extra_message_id", "msg_001");
PendingIntent replyPi = PendingIntent.getBroadcast(this, 1, replyIntent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
NotificationAction action = new NotificationAction.Builder(
R.drawable.ic_reply, "回复", replyPi).build();
Notification noti = new NotificationCompat.Builder(this, CHANNEL_MESSAGE)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("来自 Alice 的新消息")
.setContentText("你今天有空吗?")
.addAction(action) // Android 14+ 单独 Action 已废弃,可用 setStyle 内 RemoteInput
.build();
3. Direct Reply(Android 7+)
RemoteInput remoteInput = new RemoteInput.Builder("key_reply_text")
.setLabel("回复")
.build();
Intent replyIntent = new Intent(this, ReplyActionReceiver.class);
PendingIntent replyPi = PendingIntent.getBroadcast(this, 2, replyIntent,
PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
R.drawable.ic_reply, "回复", replyPi)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
Notification noti = new NotificationCompat.Builder(this, CHANNEL_MESSAGE)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Alice")
.setContentText("你今天有空吗?")
.addAction(replyAction)
.build();
注意:使用 Direct Reply 必须用
FLAG_MUTABLE,否则无法在回复时修改 PendingIntent extras。
进度通知
public class DownloadService extends Service {
private static final int NOTIFY_ID_DOWNLOAD = 2001;
private NotificationManagerCompat mNm;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@Override
public void onCreate() {
super.onCreate();
mNm = NotificationManagerCompat.from(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(() -> {
for (int progress = 0; progress <= 100; progress += 10) {
updateProgress(progress);
try {
Thread.sleep(500);
} catch (InterruptedException ignored) { }
}
stopSelf();
}).start();
return START_NOT_STICKY;
}
private void updateProgress(int progress) {
Notification noti = new NotificationCompat.Builder(this,
NotificationHelper.CHANNEL_DOWNLOAD)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("正在下载")
.setContentText(progress + "%")
.setProgress(100, progress, progress == 0)
.setOnlyAlertOnce(true)
.build();
mNm.notify(NOTIFY_ID_DOWNLOAD, noti);
}
@Override
public IBinder onBind(Intent intent) { return null; }
}
前台服务通知
public class MusicService extends Service {
private static final int NOTIFY_ID_FG = 3001;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Notification noti = new NotificationCompat.Builder(this,
NotificationHelper.CHANNEL_FOREGROUND)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("正在播放")
.setContentText("标题音乐")
.setOngoing(true) // 不能滑动清除
.build();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Android 14+ 必须指定 foregroundServiceType
startForeground(NOTIFY_ID_FG, noti,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
startForeground(NOTIFY_ID_FG, noti);
}
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) { return null; }
}
<!-- AndroidManifest.xml -->
<service
android:name=".MusicService"
android:exported="false"
android:foregroundServiceType="mediaPlayback" />
Android 13+ 通知权限
private final ActivityResultLauncher<String> mRequestNoti =
registerForActivityResult(new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) {
sendBasicNotification();
} else {
Toast.makeText(this, "通知权限被拒绝",
Toast.LENGTH_SHORT).show();
}
});
private void ensureNotificationPermissionAndSend() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
mRequestNoti.launch(Manifest.permission.POST_NOTIFICATIONS);
return;
}
}
sendBasicNotification();
}
取消通知
NotificationManagerCompat.from(this).cancel(NOTIFY_ID_BASIC); // 取消单个
NotificationManagerCompat.from(this).cancelAll(); // 取消所有
// Android 8+ 取消整个渠道下所有通知(慎用)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.deleteNotificationChannel(CHANNEL_DOWNLOAD);
}
常见坑与最佳实践
- 忘了创建 Channel 直接 notify:API 26+ 上通知静默丢失,且不会报错,调试非常痛苦。在 Application
onCreate中统一创建。 setSmallIcon用彩色图标:会在状态栏显示成一团色块。必须用单色透明 PNG。PendingIntent.FLAG_UPDATE_CURRENT漏写:复用旧 PendingIntent 时新 extra 不生效,导致跳转参数错乱。PendingIntent未指定FLAG_IMMUTABLE/MUTABLE:Android 12+ 直接抛异常崩溃。- 使用已废弃的
Notification构造器与setLatestEventInfo:API 23+ 已废弃,全部用NotificationCompat.Builder。 - 进度通知频繁刷新导致震动:必须加
setOnlyAlertOnce(true),避免每次更新都响铃。 foregroundServiceType未声明:Android 14+ 强制要求,否则startForeground抛MissingForegroundServiceTypeException,且必须在 Manifest 中声明对应权限(如FOREGROUND_SERVICE_MEDIA_PLAYBACK)。- 通知 ID 冲突:每条不同语义的通知应用不同 ID,否则后者会覆盖前者。同一会话/下载可用同一 ID 实现“更新”。
BigPictureStyle加载大图同步解码:会卡 UI 线程,应使用 Glide/Coil 异步加载。
章节小结
- NotificationChannel 是 Android 8+ 的硬性要求,importance 与 sound/vibration 等通过渠道统一管理;
- 用
NotificationCompat.Builder+setSmallIcon+setContentTitle构建基本通知; - 五种样式(BigText / BigPicture / Inbox / Messaging / Media)覆盖常见场景;
- 点击
PendingIntent与 Direct Reply(RemoteInput)实现交互; - 进度通知配合
setProgress+setOnlyAlertOnce;前台服务通知 Android 14+ 必须声明foregroundServiceType; - Android 13+ 须运行时申请
POST_NOTIFICATIONS。
下一章预告
第 16 章将进入 推送 Firebase Cloud Messaging:FCM 接入、google-services.json 配置、FirebaseMessagingService、Token 获取与刷新、前台/后台消息处理、消息类型与 Topic 订阅。