2. 创建第一个 Material 3 通知:渠道创建、通知构建、显示流程与代码模板

2.1 通知渠道原理与作用

Android 8.0(API 26)引入了通知渠道(Notification Channel)机制,所有通知必须归属于一个已注册的渠道。渠道扮演“分类容器”的角色,用户可在系统设置中独立控制每个渠道的开关、声音、振动和弹窗行为。Material 3 在 Android 12+ 上进一步将通知卡片改造为圆角背景+大图标+动态配色,但渠道机制保持不变。

属性说明设置后可否修改
渠道 ID唯一标识,字符串常量不可改
渠道名称显示给用户的渠道标签可改
重要性级别HIGH / DEFAULT / LOW / MIN不可改(需删除重建)
渠道描述用户点击“齿轮”时看到的说明文字可改
⚠ 重要性降级陷阱
渠道一旦创建,重要性级别便固化在系统中。即使用户在设置中关闭该渠道,应用也无法通过代码提升重要性。因此首次创建时务必选择正确的级别。

2.2 完整创建与显示流程概览

下图将“渠道注册 → 通知构建 → 权限申请 → 发送 → 展示”五个核心阶段串联为一条流水线。每个阶段都包含对应的 API 调用和版本适配逻辑。

Material 3 通知创建与显示流程 ① 创建通知渠道 createNotificationChannel() ② 构建通知对象 NotificationCompat.Builder ③ 检查通知权限 POST_NOTIFICATIONS(API 33+) ④ 发送通知 NotificationManager.notify() ⑤ 系统展示与交互 通知栏 → 用户点击/清除 📌 渠道须知 • 应用启动时创建 • 全局只需创建一次 • 重要性不可后期升级 • 用户可独立开关 • 删除渠道需卸载应用

2.3 渠道创建 API 与最佳实践

渠道注册必须在 Application.onCreate() 或首个 Activity 的 onCreate() 中执行,确保系统在收到第一条通知前已注册。渠道 ID 建议使用常量避免拼写错误。

// Kotlin — 渠道创建模板
class App : Application() {
    companion object {
        const val CHANNEL_GENERAL = "channel_general"
        const val CHANNEL_ALARM    = "channel_alarm"
    }
    override fun onCreate() {
        super.onCreate()
        createNotificationChannels()
    }
    private fun createNotificationChannels() {
        val general = NotificationChannel(
            CHANNEL_GENERAL,
            "一般通知",
            NotificationManager.IMPORTANCE_HIGH
        ).apply {
            description = "包含订单、物流等一般性提醒"
            enableVibration(true)
            setShowBadge(true)
        }
        val alarm = NotificationChannel(
            CHANNEL_ALARM,
            "闹钟提醒",
            NotificationManager.IMPORTANCE_HIGH
        ).apply {
            description = "闹钟与计时器到点提醒"
            enableVibration(true)
            setSound(
                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM),
                Notification.AUDIO_ATTRIBUTES_DEFAULT
            )
        }
        val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        manager.createNotificationChannel(general)
        manager.createNotificationChannel(alarm)
    }
}
💡 渠道复用检测
同一渠道 ID 重复调用 createNotificationChannel() 不会产生副作用,系统会直接忽略。因此你可以在每个 Activity 的 onCreate() 中安全调用,无需额外判重。

2.4 通知构建器与 Material 3 样式

使用 NotificationCompat.Builder 构建通知,核心参数如下表所示。Material 3 在 Android 12+ 上会自动应用圆角卡片、大图标和 Monet 动态色,但开发者仍需提供小图标和标题。

方法参数说明必填
setSmallIcon()状态栏小图标(白色单色)✔
setContentTitle()通知标题(单行)✔
setContentText()通知正文(收起状态)✔
setStyle()展开样式:BigText / BigPicture / Inbox / Messaging推荐
setPriority()兼容渠道重要性(API 26- 生效)推荐
setContentIntent()点击跳转 PendingIntent推荐
setAutoCancel()点击后自动移除通知推荐
// Kotlin — 构建通知示例
val notification = NotificationCompat.Builder(this, App.CHANNEL_GENERAL)
    .setSmallIcon(R.drawable.ic_notification)
    .setContentTitle("订单已发货")
    .setContentText("您的订单 #2024001 已发出,预计3天到达。")
    .setStyle(
        NotificationCompat.BigTextStyle()
            .bigText("您的订单 #2024001 已发出,预计3天到达。\n快递单号:SF1234567890\n点击查看物流详情。")
    )
    .setPriority(NotificationCompat.PRIORITY_HIGH)
    .setContentIntent(pendingIntent)  // 点击跳转
    .setAutoCancel(true)
    .build()

2.5 运行时权限:POST_NOTIFICATIONS

Android 13(API 33)新增 android.permission.POST_NOTIFICATIONS 运行时权限。低于 33 的设备自动授予;33+ 必须动态申请,且用户可拒绝或关闭。

// Kotlin — 权限检查与请求
private fun requestNotificationPermission() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
    if (ContextCompat.checkSelfPermission(
            this, Manifest.permission.POST_NOTIFICATIONS
        ) == PackageManager.PERMISSION_GRANTED
    ) return

    ActivityCompat.requestPermissions(
        this,
        arrayOf(Manifest.permission.POST_NOTIFICATIONS),
        PERMISSION_REQUEST_CODE
    )
}

// 在 Activity 中接收结果
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == PERMISSION_REQUEST_CODE) {
        if (grantResults.isNotEmpty() &&
            grantResults[0] == PackageManager.PERMISSION_GRANTED
        ) {
            // 授权成功,发送通知
            sendNotification()
        } else {
            // 用户拒绝,可引导至系统设置
            showPermissionDeniedDialog()
        }
    }
}
⚠ Android 13 权限行为变更
即使用户授权,后续仍可在系统设置中关闭渠道。应用无法通过代码重新开启,只能引导用户手动操作。务必在 sendNotification() 前检查 NotificationManager.areNotificationsEnabled()。

2.6 发送通知与 ID 管理

每条通知需要一个唯一 ID(整型)。覆盖相同 ID 会更新已有通知;使用新 ID 则创建新通知。推荐用 System.currentTimeMillis().toInt() 或自增计数器。

// Kotlin — 发送通知
private fun sendNotification() {
    if (!isChannelEnabled()) return  // 检查渠道是否被用户关闭

    val notificationManager =
        getSystemService(NOTIFICATION_SERVICE) as NotificationManager

    val notification = buildNotification()  // 调用构建器
    val notificationId = System.currentTimeMillis().toInt()

    notificationManager.notify(notificationId, notification)
}

private fun isChannelEnabled(): Boolean {
    val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
    val channel = manager.getNotificationChannel(App.CHANNEL_GENERAL)
    return channel != null &&
           channel.importance != NotificationManager.IMPORTANCE_NONE
}

2.7 完整代码模板(Activity 集成)

以下模板将渠道创建、权限申请、通知构建与发送合并为一个可运行的 Activity 示例。

// MainActivity.kt — 完整模板
class MainActivity : AppCompatActivity() {
    companion object {
        private const val PERMISSION_REQUEST_CODE = 1001
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 步骤1:创建渠道(已在 Application 中完成,此处仅校验)
        ensureChannelExists()

        // 步骤2:请求权限(API 33+)
        requestNotificationPermission()

        // 步骤3:点击按钮发送通知
        findViewById<Button>(R.id.btn_send).setOnClickListener {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
                ContextCompat.checkSelfPermission(
                    this, Manifest.permission.POST_NOTIFICATIONS
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                Toast.makeText(this, "请先授予通知权限", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            sendNotification()
        }
    }

    private fun ensureChannelExists() {
        val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        if (manager.getNotificationChannel(App.CHANNEL_GENERAL) == null) {
            // 若 Application 未创建,此处兜底创建
            val channel = NotificationChannel(
                App.CHANNEL_GENERAL,
                "一般通知",
                NotificationManager.IMPORTANCE_HIGH
            )
            manager.createNotificationChannel(channel)
        }
    }

    private fun requestNotificationPermission() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
        if (ContextCompat.checkSelfPermission(
                this, Manifest.permission.POST_NOTIFICATIONS
            ) == PackageManager.PERMISSION_GRANTED
        ) return
        ActivityCompat.requestPermissions(
            this,
            arrayOf(Manifest.permission.POST_NOTIFICATIONS),
            PERMISSION_REQUEST_CODE
        )
    }

    private fun sendNotification() {
        val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        val intent = Intent(this, DetailActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        val notification = NotificationCompat.Builder(this, App.CHANNEL_GENERAL)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle("订单已发货")
            .setContentText("您的订单 #2024001 已发出")
            .setStyle(NotificationCompat.BigTextStyle()
                .bigText("您的订单 #2024001 已发出\n快递单号:SF1234567890"))
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .build()

        val notificationId = System.currentTimeMillis().toInt()
        manager.notify(notificationId, notification)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == PERMISSION_REQUEST_CODE) {
            if (grantResults.isNotEmpty() &&
                grantResults[0] == PackageManager.PERMISSION_GRANTED
            ) {
                Toast.makeText(this, "通知权限已授予", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(this, "请在设置中手动开启通知权限", Toast.LENGTH_LONG).show()
            }
        }
    }
}

2.8 调试与验证方法

  • Adb 命令验证渠道:adb shell dumpsys notification 可查看所有已注册渠道及其重要性。
  • 设置界面检查:进入 系统设置 → 应用 → 你的应用 → 通知,确认渠道名称和开关状态。
  • 日志过滤:使用 Logcat 过滤 NotificationService 或 NotificationManager,观察系统是否拒绝发送。
  • 权限状态确认:adb shell appops get <包名> POST_NOTIFICATIONS 返回 allow 或 deny。

2.9 常见错误与解决方案

错误现象根因解决方案
通知不显示,无任何错误日志渠道重要性为 MIN 或 NONE创建渠道时使用 IMPORTANCE_HIGH 或 IMPORTANCE_DEFAULT
Android 13 上通知不弹未申请 POST_NOTIFICATIONS 权限动态请求权限,并检查用户是否拒绝
通知显示为“应用图标+空白标题”setSmallIcon() 使用了彩色图小图标必须为白色透明背景的单色矢量图
渠道创建后无法提升重要性渠道已固化,系统不允许升级卸载应用或清除数据后重新创建
点击通知不跳转PendingIntent 未设置 FLAG_IMMUTABLEAndroid 12+ 必须使用 FLAG_IMMUTABLE

2.10 性能与稳定性建议

  • 渠道创建时机:放在 Application.onCreate() 中,避免在每个 Activity 中重复创建。
  • 通知 ID 生成:使用 AtomicInteger 或 System.currentTimeMillis() 确保唯一,避免 ID 冲突导致通知被覆盖。
  • 权限检查前置:发送前调用 areNotificationsEnabled(),避免因权限关闭导致静默失败。
  • 避免高频发送:短时间发送大量通知会触发系统节流(NotificationManager.INTERRUPTION_FILTER_ALARMS),超过 50 条/秒可能被丢弃。
  • 兼容旧版本:API 26 以下设备忽略渠道,但仍需提供 setPriority() 控制展示行为。
📌 本章小结
创建 Material 3 通知的标准化流程:注册渠道 → 构建通知 → 请求权限(API 33+) → 发送通知。渠道是通知的“集装箱”,重要性一旦固化不可升级;构建器支持丰富样式,但小图标必须为白色单色;Android 13 起动态权限成为必选项。下一章将深入通知样式定制,包括大图文、进度条和交互按钮。