世界杯预选赛中国队赛程_世界杯多少年一次 - fybstd.com


作为 Android 开发者,你是否遇到过这些问题:通知发不出去、点击没反应、不同版本适配踩坑?Notification 作为 App 与用户沟通的 “桥梁”,既要及时传递关键信息,又要兼顾用户体验 —— 毕竟没人喜欢频繁弹窗的 “骚扰通知”。今天这篇文章,从基础配置到高级特性,再到实战避坑,带你系统掌握 Android 通知开发,让你的 App 消息推送既合规又好用。

一、开篇:为什么 Notification 对 App 至关重要?

先聊聊 “为什么要做通知开发”。对用户来说,通知是 “被动获取关键信息” 的主要方式 —— 比如微信消息、外卖进度、验证码;对开发者来说,通知是 “唤醒用户回流” 的核心手段 —— 数据显示,做好通知优化的 App,日活留存能提升 20% 以上。

但 Android 的通知系统可不是 “一成不变” 的:从 Android O(8.0)的 “通知渠道” 强制化,到 Android 13 的 “运行时权限”,再到 Android 14 的 “精准设置跳转”,每一次版本迭代都在平衡 “功能” 与 “用户体验”。所以,想做好通知开发,“版本适配” 和 “用户体验” 这两个点必须贯穿始终。

二、基础必备:权限与渠道配置(避坑第一步)

开发通知的第一步,不是写代码,而是搞定 “权限” 和 “渠道”—— 这也是新手最容易踩坑的地方。

2.1 权限配置:不同版本大不同

Android 对通知权限的管控越来越严,核心分水岭在Android 13(API 33),之前是 “默认授予”,之后需要 “主动请求”。

(1)Android 12 及以下(API ≤32)

无需动态请求,只需在AndroidManifest.xml声明基础权限:

(2)Android 13 及以上(API ≥33)

必须声明并动态请求POST_NOTIFICATIONS权限,否则通知直接 “隐身”:

动态请求权限代码(Kotlin):

object NotificationPermissionHelper {

private const val REQUEST_CODE = 1001

// 检查并请求通知权限

fun checkAndRequest(activity: AppCompatActivity) {

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return

val permission = Manifest.permission.POST_NOTIFICATIONS

when (ActivityCompat.checkSelfPermission(activity, permission)) {

PackageManager.PERMISSION_GRANTED -> {

// 权限已授予,可发送通知

}

PackageManager.PERMISSION_DENIED -> {

// 首次请求或用户未勾选“不再询问”

if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {

// 显示“为什么需要权限”的弹窗(提升授权率)

showRationaleDialog(activity)

} else {

// 发起权限请求

ActivityCompat.requestPermissions(

activity,

arrayOf(permission),

REQUEST_CODE

)

}

}

PackageManager.PERMISSION_DENIED_APP_OP -> {

// 用户通过系统设置禁用了权限,引导到设置页

jumpToNotificationSettings(activity)

}

}

}

// 权限说明弹窗

private fun showRationaleDialog(activity: AppCompatActivity) {

AlertDialog.Builder(activity)

.setTitle("需要通知权限")

.setMessage("开启后可及时接收消息提醒,不错过重要内容")

.setPositiveButton("去授权") { _, _ ->

ActivityCompat.requestPermissions(

activity,

arrayOf(Manifest.permission.POST_NOTIFICATIONS),

REQUEST_CODE

)

}

.setNegativeButton("取消", null)

.show()

}

// 跳转至App通知设置页

fun jumpToNotificationSettings(context: Context) {

val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {

putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)

// 可选:直接跳转至指定渠道(Android 14+支持)

// putExtra(Settings.EXTRA_CHANNEL_ID, "your_channel_id")

}

context.startActivity(intent)

}

}

小贴士:权限请求别在 “App 启动时” 弹,最好在 “用户需要接收通知” 的场景触发(比如点击 “开启消息提醒” 按钮),这样授权率会高很多。

2.2 通知渠道:Android O + 的 “必选项”

从 Android O(8.0,API 26)开始,所有通知必须归属一个 “渠道”,否则通知直接不显示。为什么要加渠道?本质是把 “通知控制权” 交给用户 —— 用户可以单独关闭某类通知(比如 “广告通知”),而不用关闭 App 所有通知。

(1)渠道核心特性

不可修改的属性:渠道 ID、重要性(决定通知是否弹窗 / 发声)、锁屏可见性;

可修改的属性:渠道名称、描述、声音、震动;

用户控制:在 “系统设置 - App 通知” 中,用户可单独开关渠道、调整声音震动。

(2)创建渠道代码(Kotlin)

建议在 App 启动时初始化所有渠道,避免动态创建导致通知丢失:

object NotificationChannelHelper {

// 渠道ID:按功能分类(建议用包名+功能,避免冲突)

const val CHANNEL_ID_MESSAGE = "com.example.app.notification.message" // 聊天消息

const val CHANNEL_ID_PROGRESS = "com.example.app.notification.progress" // 进度通知

const val CHANNEL_ID_SYSTEM = "com.example.app.notification.system" // 系统提示

// 初始化所有渠道

fun init(context: Context) {

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return

val notificationManager = context.getSystemService(NotificationManager::class.java)

val channels = listOf(

// 1. 聊天消息渠道(高重要性:弹窗+发声)

createMessageChannel(),

// 2. 进度通知渠道(中重要性:发声不弹窗)

createProgressChannel(),

// 3. 系统提示渠道(低重要性:无声音,仅状态栏显示)

createSystemChannel()

)

notificationManager.createNotificationChannels(channels)

}

// 聊天消息渠道

private fun createMessageChannel(): NotificationChannel {

return NotificationChannel(

CHANNEL_ID_MESSAGE,

"聊天消息", // 渠道名称(用户可见)

NotificationManager.IMPORTANCE_HIGH // 高重要性

).apply {

description = "接收好友消息、评论回复等通知" // 渠道描述(用户可见)

setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null)

enableVibration(true)

vibrationPattern = longArrayOf(0, 100, 200, 100) // 震动模式:0ms延迟,震100ms,停200ms,震100ms

lightColor = Color.BLUE // 通知灯颜色(需设备支持)

lockscreenVisibility = Notification.VISIBILITY_PUBLIC // 锁屏显示完整内容

setShowBadge(true) // 显示App图标徽章(未读消息数)

}

}

// 进度通知渠道(如下载、同步)

private fun createProgressChannel(): NotificationChannel {

return NotificationChannel(

CHANNEL_ID_PROGRESS,

"进度通知",

NotificationManager.IMPORTANCE_DEFAULT

).apply {

description = "显示下载、上传、同步等进度"

setSound(null, null) // 禁用声音

enableVibration(false) // 禁用震动

lockscreenVisibility = Notification.VISIBILITY_PRIVATE // 锁屏隐藏内容

}

}

// 系统提示渠道(如版本更新、缓存清理)

private fun createSystemChannel(): NotificationChannel {

return NotificationChannel(

CHANNEL_ID_SYSTEM,

"系统提示",

NotificationManager.IMPORTANCE_LOW

).apply {

description = "接收App更新、设置变更等通知"

setSound(null, null)

enableVibration(false)

lockscreenVisibility = Notification.VISIBILITY_SECRET // 锁屏不显示

}

}

}

避坑提醒:渠道 ID 一旦确定,后续版本别修改!否则用户之前的渠道设置会失效,相当于 “新建了一个渠道”。

三、核心实战:从基础通知到高级特性

搞定权限和渠道后,就可以开始写通知代码了。下面从 “基础通知” 到 “高级特性”,一步步拆解实战技巧。

3.1 基础通知:3 步实现 “点击跳转”

最常见的通知场景:显示标题 + 内容,点击跳转到 App 指定页面。核心用NotificationCompat.Builder(AndroidX 兼容类,适配低版本)。

实现代码(Kotlin)

class BasicNotificationHelper(private val context: Context) {

private val notificationManager = context.getSystemService(NotificationManager::class.java)

// 构建并发送基础通知

fun send(

title: String,

content: String,

targetActivity: Class<*>, // 点击跳转的页面

notificationId: Int = 100 // 通知唯一ID(用于更新/取消)

) {

// 1. 配置点击跳转的PendingIntent

val intent = Intent(context, targetActivity).apply {

flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP

putExtra("notification_id", notificationId) // 传递参数(可选)

}

val pendingIntent = PendingIntent.getActivity(

context,

notificationId, // requestCode:用通知ID避免冲突

intent,

// Android 12+ 必需指定IMMUTABLE/MUTABLE:这里用IMMUTABLE(无动态数据修改)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

} else {

PendingIntent.FLAG_UPDATE_CURRENT

}

)

// 2. 构建通知(关联“聊天消息”渠道)

val notification = NotificationCompat.Builder(context, NotificationChannelHelper.CHANNEL_ID_MESSAGE)

.setSmallIcon(R.drawable.ic_notification_small) // 必需:小图标(建议白色透明矢量图)

.setContentTitle(title) // 标题(不超过30字)

.setContentText(content) // 内容(不超过60字)

.setContentIntent(pendingIntent) // 点击触发的Intent

.setPriority(NotificationCompat.PRIORITY_HIGH) // 优先级(Android N及以下生效)

.setAutoCancel(true) // 点击后自动取消通知

.setWhen(System.currentTimeMillis()) // 显示时间(默认当前时间)

.setOnlyAlertOnce(true) // 同一ID通知多次发送,仅首次发声震动

.build()

// 3. 发送通知

notificationManager.notify(notificationId, notification)

}

// 取消通知

fun cancel(notificationId: Int) {

notificationManager.cancel(notificationId)

}

}

使用示例:

// 在Activity中调用

val helper = BasicNotificationHelper(this)

helper.send(

title = "新消息",

content = "张三:今天有空一起吃饭吗?",

targetActivity = ChatActivity::class.java,

notificationId = 1001

)

关键注意点:

setSmallIcon是 “必需属性”,缺失会导致通知不显示;建议用白色透明矢量图(vector drawable),避免系统渲染后细节丢失;

PendingIntent的requestCode用 “通知 ID”,避免不同通知的PendingIntent复用导致参数覆盖。

3.2 高级特性 1:进度通知(下载 / 同步场景)

需要显示 “进度条” 的场景(如下载文件、数据同步),核心用setProgress方法,支持 “确定进度”(如 50%)和 “不确定进度”(如 “加载中” 转圈)。

实现代码(Kotlin)

class ProgressNotificationHelper(private val context: Context) {

private val notificationManager = context.getSystemService(NotificationManager::class.java)

private val notificationId = 200 // 固定ID(用于更新进度)

// 初始化“下载中”通知

fun initDownload(fileName: String): Notification {

return NotificationCompat.Builder(context, NotificationChannelHelper.CHANNEL_ID_PROGRESS)

.setSmallIcon(R.drawable.ic_download)

.setContentTitle("正在下载")

.setContentText("$fileName (0%)")

.setProgress(100, 0, false) // 总进度100,当前0,不确定进度=false

.setOngoing(true) // 持续通知:不可手动滑动删除

.setAutoCancel(false)

.build()

}

// 更新进度(子线程中调用)

fun updateProgress(fileName: String, progress: Int, isCompleted: Boolean = false) {

val builder = NotificationCompat.Builder(context, NotificationChannelHelper.CHANNEL_ID_PROGRESS)

.setSmallIcon(R.drawable.ic_download)

.setContentTitle(if (isCompleted) "下载完成" else "正在下载")

.setOngoing(!isCompleted) // 完成后允许手动删除

.setAutoCancel(isCompleted)

if (isCompleted) {

// 下载完成:移除进度条,添加“打开文件”按钮

builder.setContentText("$fileName 已下载完成")

.setProgress(0, 0, false) // 进度条消失

.addAction(buildOpenFileAction(fileName))

} else {

// 下载中:更新进度

builder.setContentText("$fileName ($progress%)")

.setProgress(100, progress, false)

}

// 发送更新后的通知

notificationManager.notify(notificationId, builder.build())

}

// 构建“打开文件”按钮动作

private fun buildOpenFileAction(fileName: String): NotificationCompat.Action {

val intent = Intent(context, FileOpenActivity::class.java).apply {

putExtra("file_name", fileName)

flags = Intent.FLAG_ACTIVITY_NEW_TASK

}

val pendingIntent = PendingIntent.getActivity(

context,

notificationId + 2, // requestCode与通知ID区分

intent,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

} else {

PendingIntent.FLAG_UPDATE_CURRENT

}

)

return NotificationCompat.Action(

R.drawable.ic_open_file, // 按钮图标

"打开文件", // 按钮文本

pendingIntent

)

}

}

使用示例(模拟下载):

// 在子线程中更新进度(如Coroutine)

fun startDownload(context: Context, fileName: String) {

val helper = ProgressNotificationHelper(context)

// 1. 发送初始通知

val initNotification = helper.initDownload(fileName)

context.getSystemService(NotificationManager::class.java).notify(helper.notificationId, initNotification)

// 2. 模拟下载进度(实际项目替换为真实下载逻辑)

CoroutineScope(Dispatchers.IO).launch {

for (progress in 0..100 step 10) {

delay(1000) // 模拟1秒下载10%

helper.updateProgress(fileName, progress)

}

// 3. 下载完成

helper.updateProgress(fileName, 100, isCompleted = true)

}

}

3.3 高级特性 2:快速回复(聊天场景)

用户收到聊天通知时,无需打开 App,直接在通知栏输入回复 —— 核心用RemoteInput实现输入框,BroadcastReceiver接收回复内容。

实现代码(Kotlin)

class ReplyNotificationHelper(private val context: Context) {

private val notificationId = 300

private val notificationManager = context.getSystemService(NotificationManager::class.java)

// 构建带快速回复的通知

fun send(contactName: String, lastMessage: String) {

// 1. 配置快速回复输入框

val remoteInput = RemoteInput.Builder("reply_text") // 输入内容的Key

.setLabel("输入回复...") // 输入框提示文本

.setChoices(arrayOf("好的", "马上来", "稍后聊")) // 快捷回复选项(可选)

.build()

// 2. 配置回复动作(点击“发送”触发广播)

val replyIntent = Intent(context, ReplyReceiver::class.java).apply {

action = "com.example.app.NOTIFICATION_REPLY"

putExtra("contact_name", contactName)

putExtra("notification_id", notificationId)

}

val replyPendingIntent = PendingIntent.getBroadcast(

context,

notificationId + 1,

replyIntent,

// 必需用FLAG_MUTABLE:接收用户输入的动态数据

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE

} else {

PendingIntent.FLAG_UPDATE_CURRENT

}

)

// 3. 构建回复动作按钮

val replyAction = NotificationCompat.Action.Builder(

R.drawable.ic_reply, // 按钮图标

"回复", // 按钮文本

replyPendingIntent

)

.addRemoteInput(remoteInput) // 绑定输入框

.setShowsUserInterface(false) // 无需打开App界面

.build()

// 4. 构建并发送通知

val notification = NotificationCompat.Builder(context, NotificationChannelHelper.CHANNEL_ID_MESSAGE)

.setSmallIcon(R.drawable.ic_notification_small)

.setContentTitle(contactName)

.setContentText(lastMessage)

.setContentIntent(buildChatIntent(contactName)) // 点击跳转聊天页

.addAction(replyAction) // 添加回复按钮

.setAutoCancel(true)

.setPriority(NotificationCompat.PRIORITY_HIGH)

.build()

notificationManager.notify(notificationId, notification)

}

// 构建聊天页跳转Intent

private fun buildChatIntent(contactName: String): PendingIntent {

val intent = Intent(context, ChatActivity::class.java).apply {

putExtra("contact_name", contactName)

flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP

}

return PendingIntent.getActivity(

context,

notificationId,

intent,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

} else {

PendingIntent.FLAG_UPDATE_CURRENT

}

)

}

// 接收快速回复的广播接收器

class ReplyReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {

if (intent?.action != "com.example.app.NOTIFICATION_REPLY") return

val contactName = intent.getStringExtra("contact_name") ?: return

val notificationId = intent.getIntExtra("notification_id", -1)

if (notificationId == -1) return

// 1. 获取用户回复的内容

val replyText = RemoteInput.getResultsFromIntent(intent)

?.getString("reply_text") ?: return

// 2. 处理回复(如发送到服务器)

CoroutineScope(Dispatchers.IO).launch {

sendReplyToServer(contactName, replyText)

// 3. 回复成功后,更新通知为“已回复”

updateNotification(context!!, contactName, replyText, notificationId)

}

}

// 模拟发送回复到服务器

private suspend fun sendReplyToServer(contactName: String, replyText: String) {

delay(1000) // 模拟网络请求

Log.d("ReplyReceiver", "向 $contactName 发送回复:$replyText")

}

// 更新通知为“已回复”状态

private fun updateNotification(

context: Context,

contactName: String,

replyText: String,

notificationId: Int

) {

val notification = NotificationCompat.Builder(context, NotificationChannelHelper.CHANNEL_ID_MESSAGE)

.setSmallIcon(R.drawable.ic_notification_small)

.setContentTitle(contactName)

.setContentText("你:$replyText")

.setContentIntent(buildChatIntent(context, contactName))

.setAutoCancel(true)

.build()

context.getSystemService(NotificationManager::class.java)

.notify(notificationId, notification)

}

private fun buildChatIntent(context: Context, contactName: String): PendingIntent {

// 同上文buildChatIntent逻辑

}

}

}

注意:广播接收器需要在AndroidManifest.xml中注册(或动态注册):

android:name=".ReplyNotificationHelper$ReplyReceiver"

android:exported="false">

3.4 高级特性 3:自定义布局(突破系统样式)

系统默认样式满足不了需求?可以用RemoteViews自定义通知布局(支持普通视图和展开大视图)。

关键限制

仅支持LinearLayout/RelativeLayout,不支持ConstraintLayout;

控件 ID 需全局唯一,避免与系统冲突;

尺寸单位用dp,适配不同设备。

示例:自定义通知布局(layout_notification_custom.xml)

android:layout_width="match_parent"

android:layout_height="64dp"

android:gravity="center_vertical"

android:paddingHorizontal="16dp"

android:orientation="horizontal">

android:id="@+id/iv_icon"

android:layout_width="40dp"

android:layout_height="40dp"

android:src="@drawable/ic_custom_notification"

android:scaleType="centerCrop"/>

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_weight="1"

android:layout_marginStart="12dp"

android:orientation="vertical">

android:id="@+id/tv_title"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textSize="16sp"

android:textColor="@color/black"

android:textStyle="bold"/>

android:id="@+id/tv_content"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textSize="14sp"

android:textColor="@color/gray"

android:layout_marginTop="2dp"/>

android:id="@+id/btn_view"

android:layout_width="wrap_content"

android:layout_height="32dp"

android:text="查看详情"

android:textSize="12sp"

android:backgroundTint="@color/blue"

android:textColor="@color/white"/>

自定义通知构建代码(Kotlin)

class CustomNotificationHelper(private val context: Context) {

fun send(

title: String,

content: String,

iconResId: Int,

notificationId: Int = 400

) {

// 1. 加载自定义布局

val remoteViews = RemoteViews(context.packageName, R.layout.layout_notification_custom)

// 2. 设置布局内容

remoteViews.setImageViewResource(R.id.iv_icon, iconResId)

remoteViews.setTextViewText(R.id.tv_title, title)

remoteViews.setTextViewText(R.id.tv_content, content)

// 3. 绑定按钮点击事件(通过广播)

val btnIntent = Intent(context, CustomReceiver::class.java).apply {

action = "com.example.app.NOTIFICATION_CUSTOM_ACTION"

putExtra("notification_id", notificationId)

}

val btnPendingIntent = PendingIntent.getBroadcast(

context,

notificationId + 1,

btnIntent,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

} else {

PendingIntent.FLAG_UPDATE_CURRENT

}

)

remoteViews.setOnClickPendingIntent(R.id.btn_view, btnPendingIntent)

// 4. 绑定通知整体点击事件(跳转页面)

val contentIntent = Intent(context, DetailActivity::class.java).apply {

putExtra("notification_id", notificationId)

}

val contentPendingIntent = PendingIntent.getActivity(

context,

notificationId,

contentIntent,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

} else {

PendingIntent.FLAG_UPDATE_CURRENT

}

)

// 5. 构建并发送通知

val notification = NotificationCompat.Builder(context, NotificationChannelHelper.CHANNEL_ID_MESSAGE)

.setSmallIcon(R.drawable.ic_notification_small) // 必需:系统小图标

.setCustomContentView(remoteViews) // 设置自定义布局

.setContentIntent(contentPendingIntent)

.setAutoCancel(true)

.setPriority(NotificationCompat.PRIORITY_HIGH)

.build()

context.getSystemService(NotificationManager::class.java)

.notify(notificationId, notification)

}

// 处理自定义按钮点击的广播接收器

class CustomReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {

if (intent?.action != "com.example.app.NOTIFICATION_CUSTOM_ACTION") return

val notificationId = intent.getIntExtra("notification_id", -1)

if (notificationId == -1) return

// 处理“查看详情”逻辑(如跳转详情页)

val intent = Intent(context, DetailActivity::class.java).apply {

flags = Intent.FLAG_ACTIVITY_NEW_TASK

putExtra("notification_id", notificationId)

}

context?.startActivity(intent)

// 取消通知

context?.getSystemService(NotificationManager::class.java)

?.cancel(notificationId)

}

}

}

四、版本适配:这些坑一定要避开!

Android 各版本对通知的限制不同,下面总结几个关键版本的适配要点,帮你少踩坑。

4.1 Android 12(API 31):PendingIntent 必须加 Flag

从 Android 12 开始,所有PendingIntent必须指定FLAG_IMMUTABLE或FLAG_MUTABLE,否则会抛IllegalArgumentException。

FLAG_IMMUTABLE:不可修改,适用于 “无动态数据更新” 的场景(如跳转固定页面);

FLAG_MUTABLE:可修改,适用于 “需要接收动态数据” 的场景(如快速回复、RemoteInput)。

错误示例(Android 12 + 崩溃):

// 错误:未指定IMMUTABLE/MUTABLE

val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

正确示例:

val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE // 无动态数据

// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE // 有动态数据

} else {

PendingIntent.FLAG_UPDATE_CURRENT

}

val pendingIntent = PendingIntent.getActivity(context, 0, intent, flags)

4.2 Android 13(API 33):通知权限必须动态请求

如前文 2.1 节所述,targetSdkVersion ≥33 时,必须动态请求POST_NOTIFICATIONS权限,否则通知不显示且不抛异常 —— 很多开发者调试时以为代码错了,其实是权限没加。

避坑技巧:发送通知前,先检查权限:

fun canSendNotification(context: Context): Boolean {

return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

ActivityCompat.checkSelfPermission(

context,

Manifest.permission.POST_NOTIFICATIONS

) == PackageManager.PERMISSION_GRANTED

} else {

true // Android 12及以下默认有权限

}

}

4.3 Android 14(API 34):支持精准跳转渠道设置

Android 14 新增了 “直接跳转至指定渠道设置页” 的功能,用户体验更好。适配代码:

fun jumpToChannelSettings(context: Context, channelId: String) {

val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {

putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)

putExtra(Settings.EXTRA_CHANNEL_ID, channelId) // 跳转至指定渠道

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {

// 可选:指定渠道组(如有)

putExtra(Settings.EXTRA_NOTIFICATION_CHANNEL_GROUP_ID, "your_group_id")

}

}

context.startActivity(intent)

}

五、最佳实践:让你的通知既合规又受欢迎

做好通知开发,不仅要 “能发出去”,还要 “用户愿意看”。下面分享几个最佳实践,帮你平衡 “功能” 与 “用户体验”。

5.1 设计规范:别让用户反感你的通知

小图标:用白色透明矢量图,避免彩色图标(系统可能渲染为白色,丢失细节);

文本长度:标题≤30 字,内容≤60 字,过长会被系统截断(需展开查看的用BigTextStyle);

频率控制:同类通知(如广告)别短时间内多次发送,提供 “免打扰” 开关;

时效性:临时通知(如验证码)设置setTimeoutAfter(300000)(5 分钟后自动取消),避免占用通知栏。

5.2 性能优化:别让通知拖慢 App

避免主线程操作:图片加载、数据解析等耗时操作放子线程,用 Glide/Picasso 异步加载通知图片;

复用通知 ID:同类通知(如进度通知、音乐播放)复用同一 ID,避免创建过多实例;

及时取消无用通知:已完成的下载、已读的消息,及时调用cancel()移除,减少系统资源占用。

5.3 常见问题排查:通知不显示怎么办?

遇到通知不显示,按以下步骤排查:

查权限:Android 13 + 确认POST_NOTIFICATIONS已授予;查渠道:Android O + 确认通知关联了渠道,且渠道未被用户禁用;查小图标:确认setSmallIcon已设置,且是有效资源;查 PendingIntent:Android 12 + 确认加了IMMUTABLE/MUTABLE Flag;查日志:用adb logcat | grep Notification看系统日志,定位具体错误(如渠道不存在、权限被拒)。

六、结尾:未来趋势与总结

随着 Android 系统对 “用户隐私” 和 “体验” 的重视,通知开发会越来越强调 “用户控制”—— 比如未来可能会有更细粒度的权限(如 “仅允许重要通知”)、更智能的通知排序(按用户兴趣)。

总结一下本文的核心:

基础必备:权限(Android 13 + 动态请求)、渠道(Android O + 必加);核心功能:基础通知(点击跳转)、进度通知、快速回复、自定义布局;版本适配:Android 12 的 PendingIntent Flag、Android 13 的权限、Android 14 的设置跳转;最佳实践:控制频率、优化性能、关注用户体验。

希望这篇文章能帮你搞定 Android 通知开发,让你的 App 消息推送既合规又好用。如果有其他问题,欢迎在评论区交流~