19. 通知与消息推送:让用户随时感知你的存在

通知,是 Android 应用与用户沟通的「最后一公里」。做得好了,用户觉得贴心;做得差了,用户直接关掉通知权限,甚至卸载应用。我这些年做项目,踩过不少通知的坑,今天就把这些经验掰开揉碎讲给你听。

本章核心知识点

  • Notification 通知构建:从零到一创建通知
  • NotificationChannel:Android 8.0 后的必知概念
  • Android 12+ 通知适配:新版本的新规矩
  • FCM 集成:云端推送的标配方案
  • 本地通知:不依赖网络也能发通知
通知与消息推送知识体系 通知 Notification 构建 Builder · 样式 · 点击 NotificationChannel 重要性 · 分组 · 适配 Android 12+ 适配 通知权限 · 运行时请求 FCM + 本地通知 云端推送 · 本地调度

19.1 Notification 通知构建:从零开始

通知说白了就是一个显示在系统栏的 UI 组件。Android 提供了 NotificationCompat.Builder 来帮我们构建,兼容性做得不错。

最基本的通知长这样:

// 创建通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
    .setSmallIcon(R.drawable.ic_notification)
    .setContentTitle("新消息")
    .setContentText("你有一条未读消息")
    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
    .setAutoCancel(true);

NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(NOTIFICATION_ID, builder.build());

嗯,这里要注意 setAutoCancel(true)。我刚开始做的时候忘了加这个,用户点击通知后通知还在那挂着,被测试骂了好几次。加上之后,点击通知自动消失,体验好很多。

小技巧NOTIFICATION_ID 最好用常量管理,同一个 ID 会覆盖之前的通知。如果你想展示多条通知,每个 ID 必须不同。

19.2 通知渠道:Android 8.0 的分水岭

Android 8.0(API 26)引入了 NotificationChannel。我个人觉得这是 Android 通知史上最重要的改动。为什么?因为它把通知的控制权还给了用户。

以前开发者想怎么发就怎么发,用户只能全部关掉或全部打开。现在每个渠道都可以单独控制——用户可以把「促销通知」关掉,但保留「订单通知」。

// 创建通知渠道(只需创建一次)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    CharSequence name = "订单通知";
    String description = "包含订单状态变更的通知";
    int importance = NotificationManager.IMPORTANCE_DEFAULT;
    NotificationChannel channel = new NotificationChannel(
        CHANNEL_ORDER, name, importance
    );
    channel.setDescription(description);
    
    NotificationManager manager = getSystemService(NotificationManager.class);
    manager.createNotificationChannel(channel);
}

这里有个坑:渠道一旦创建,重要性等级就不能改了。除非用户卸载重装。所以创建渠道前,想清楚这个渠道到底该多重要。

重要性等级 行为表现 适用场景
IMPORTANCE_HIGH 弹出横幅、有声音、显示在锁屏 即时消息、来电
IMPORTANCE_DEFAULT 有声音、显示在通知栏 订单通知、系统提醒
IMPORTANCE_LOW 无声音、显示在通知栏 订阅更新、内容推荐
IMPORTANCE_MIN 无声音、折叠在底部 后台任务完成提示

避坑指南:我曾经在一个项目中把所有通知都设为 HIGH 重要性,结果用户直接关掉了整个应用的通知权限。后来改成按场景分级,用户投诉少了 70%。别滥用 HIGH,用户不是傻子。

19.3 Android 12+ 通知适配:新版本的新规矩

Android 12 开始,通知权限变成了运行时权限。这意味着你需要像请求相机权限一样,主动请求通知权限。

具体来说,Android 13(API 33)引入了 POST_NOTIFICATIONS 权限。如果你的 targetSdk 是 33 及以上,必须在运行时请求这个权限。

// Android 13+ 需要运行时请求通知权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
            != PackageManager.PERMISSION_GRANTED) {
        ActivityResultLauncher<String> launcher = 
            registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
                if (isGranted) {
                    // 发送通知
                } else {
                    // 引导用户去设置开启
                }
            });
        launcher.launch(Manifest.permission.POST_NOTIFICATIONS);
    }
}

你想想看,如果用户拒绝了权限,你的通知就发不出去了。这时候怎么办?我建议在用户拒绝后,弹个 Dialog 引导他去系统设置里手动开启。别直接放弃,也别反复弹窗惹人烦。

Android 12 还有一个变化:通知的「快速设置」按钮可以一键关闭所有通知。所以你的通知渠道设计要合理,否则用户一键全关,你连解释的机会都没有。

19.4 FCM 集成:云端推送的标配

Firebase Cloud Messaging(FCM)是 Google 官方的推送方案。说白了就是你的服务器把消息发给 FCM 服务器,FCM 再推送到用户手机。省去了你自己维护长连接的麻烦。

集成步骤其实不复杂:

  1. 在 Firebase 控制台创建项目,下载 google-services.json
  2. build.gradle 添加依赖
  3. 创建 FirebaseMessagingService 子类
  4. 获取设备 Token 并上传到自己的服务器
// 1. 添加依赖(app/build.gradle)
implementation 'com.google.firebase:firebase-messaging:23.2.0'

// 2. 创建服务
public class MyFirebaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onNewToken(String token) {
        // 新 Token 生成时,上传到自己的服务器
        sendTokenToServer(token);
    }
    
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // 收到消息,构建通知
        if (remoteMessage.getNotification() != null) {
            showNotification(remoteMessage.getNotification());
        }
        // 处理数据消息
        if (remoteMessage.getData().size() > 0) {
            handleDataMessage(remoteMessage.getData());
        }
    }
}

这里有个细节:FCM 消息分两种——通知消息(系统自动展示)和数据消息(需要自己处理)。通知消息在应用后台时由系统直接展示,但应用前台时不会自动弹出。所以很多人会在 onMessageReceived 里自己构建通知,保证前后台行为一致。

个人经验:我习惯把 FCM 的 Token 存在 SharedPreferences 里,每次启动时检查是否变化。如果变了就更新服务器。这样能避免 Token 过期导致推送失败。

19.5 本地通知:不依赖网络也能发

本地通知就是应用自己调度的通知,不需要服务器参与。典型场景:闹钟提醒、定时任务完成、倒计时结束。

Android 提供了 AlarmManagerWorkManager 两种方式来实现定时通知。我个人更推荐 WorkManager,因为它能处理设备重启后的恢复。

// 使用 WorkManager 发送本地通知
public class NotificationWorker extends Worker {
    public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
    }
    
    @NonNull
    @Override
    public Result doWork() {
        // 构建并发送通知
        NotificationManager manager = (NotificationManager) 
            getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
        Notification notification = new NotificationCompat.Builder(
            getApplicationContext(), CHANNEL_ID
        )
        .setSmallIcon(R.drawable.ic_alarm)
        .setContentTitle("提醒")
        .setContentText("该喝水了!")
        .build();
        manager.notify(1001, notification);
        return Result.success();
    }
}

// 调度任务:1小时后执行
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(NotificationWorker.class)
    .setInitialDelay(1, TimeUnit.HOURS)
    .build();
WorkManager.getInstance(context).enqueue(workRequest);

嗯,这里要注意:Android 12 开始,精确闹钟需要权限。如果你的通知对时间要求很严格(比如吃药提醒),需要申请 SCHEDULE_EXACT_ALARM 权限。否则就用 WorkManager 的近似调度,系统会帮你省电。

避坑指南:我曾经用 AlarmManager 做本地通知,结果用户重启手机后所有通知都丢了。后来换成 WorkManager,它自动处理了设备重启后的重新调度。所以能用 WorkManager 就别用 AlarmManager。

19.6 通知的最佳实践

最后,分享几个我这些年总结的通知经验:

  • 渠道数量控制在 3-5 个:太多用户会烦,太少不够精细
  • 通知内容要精简:标题不超过 30 字,内容不超过 60 字
  • 善用通知分组:同类型的通知折叠在一起,比如「3 条新消息」
  • 不要滥用高优先级:只有真正紧急的消息才用 HIGH
  • 测试所有 Android 版本:不同版本的通知行为差异很大

通知这件事,说白了就是「在合适的时间,用合适的方式,告诉用户合适的信息」。做得好,用户觉得贴心;做不好,用户直接关权限。希望今天的分享能帮你少走弯路。


公众号:蓝海资料掘金营,微信deep3321