13、扩展函数与高阶函数在通知与广播中的应用
通知和广播,这两个东西在 Android 开发里太常见了。但说实话,每次写 Notification 的代码,我都觉得有点烦。Builder 链式调用虽然还行,但一旦涉及渠道管理、点击跳转、分组折叠,代码就开始变得又臭又长。
广播接收器也是。注册、注销、处理 Intent,模板代码一堆。我早期做项目时,经常因为忘记在 onPause 里注销广播而引发内存泄漏。嗯,后来我学乖了,用高阶函数封装一下,清爽多了。
这一章,我们就用扩展函数和高阶函数,把通知和广播的代码写得像 DSL 一样优雅。
13.1 通知渠道管理的痛点
Android 8.0 之后,通知必须绑定渠道。每个渠道有 id、名称、重要性等级。如果你的 App 有七八种通知类型,每个渠道都要重复写这些配置。
我见过最夸张的项目,一个渠道配置写了 30 行。其实说白了,渠道就是一个数据类加一个注册方法。我们用扩展函数把它简化掉。
13.2 用扩展函数封装渠道创建
先定义一个渠道的 DSL 构建器:
class NotificationChannelBuilder(val id: String, val name: String) {
var description: String = ""
var importance: Int = NotificationManager.IMPORTANCE_DEFAULT
var enableVibration: Boolean = true
var vibrationPattern: LongArray? = null
var enableLights: Boolean = false
var lightColor: Int = Color.TRANSPARENT
var lockscreenVisibility: Int = NotificationCompat.VISIBILITY_PUBLIC
fun build(): NotificationChannel {
val channel = NotificationChannel(id, name, importance)
channel.description = description
channel.enableVibration(enableVibration)
vibrationPattern?.let { channel.vibrationPattern = it }
channel.enableLights(enableLights)
channel.lightColor = lightColor
channel.lockscreenVisibility = lockscreenVisibility
return channel
}
}
然后给 NotificationManager 加一个扩展函数:
fun NotificationManager.createChannel(
id: String,
name: String,
block: NotificationChannelBuilder.() -> Unit
) {
val builder = NotificationChannelBuilder(id, name)
builder.block()
createNotificationChannel(builder.build())
}
使用的时候,就像这样:
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createChannel("order", "订单通知") {
description = "订单状态变更通知"
importance = NotificationManager.IMPORTANCE_HIGH
enableVibration = true
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
}
manager.createChannel("promotion", "促销活动") {
description = "优惠券和促销信息"
importance = NotificationManager.IMPORTANCE_DEFAULT
enableVibration = false
}
我个人习惯把渠道配置集中在一个地方管理。这样后期改重要性等级,或者加新渠道,都特别方便。你想想看,如果每个渠道都 new 一个 NotificationChannel 对象,再 set 一堆属性,那代码得多冗余。
13.3 Notification 的 DSL 构建
通知本身的构建,也可以用同样的思路。我们给 NotificationCompat.Builder 加扩展:
class NotificationBuilder(val context: Context, val channelId: String) {
var title: String = ""
var content: String = ""
var smallIcon: Int = 0
var largeIcon: Bitmap? = null
var autoCancel: Boolean = true
var priority: Int = NotificationCompat.PRIORITY_DEFAULT
var pendingIntent: PendingIntent? = null
var actions: MutableList<NotificationCompat.Action> = mutableListOf()
var style: NotificationCompat.Style? = null
fun action(icon: Int, title: String, intent: PendingIntent) {
actions.add(NotificationCompat.Action(icon, title, intent))
}
fun build(): Notification {
val builder = NotificationCompat.Builder(context, channelId)
.setContentTitle(title)
.setContentText(content)
.setSmallIcon(smallIcon)
.setAutoCancel(autoCancel)
.setPriority(priority)
largeIcon?.let { builder.setLargeIcon(it) }
pendingIntent?.let { builder.setContentIntent(it) }
style?.let { builder.setStyle(it) }
actions.forEach { builder.addAction(it) }
return builder.build()
}
}
fun NotificationManager.sendNotification(
context: Context,
channelId: String,
id: Int,
block: NotificationBuilder.() -> Unit
) {
val builder = NotificationBuilder(context, channelId)
builder.block()
notify(id, builder.build())
}
调用的时候,简直不要太爽:
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.sendNotification(this, "order", 1001) {
title = "订单已发货"
content = "您的包裹正在路上,预计明天到达"
smallIcon = R.drawable.ic_shipping
autoCancel = true
pendingIntent = createPendingIntent(...)
action(R.drawable.ic_track, "查看物流", createTrackIntent())
}
我在项目中遇到过一种情况:产品经理要求同一个通知在不同状态下显示不同的按钮。用传统写法,我得写好几个 if-else 分支来构建 Builder。用 DSL 之后,我只需要根据状态配置不同的 action 列表就行,代码干净多了。
13.4 BroadcastReceiver 的简化
广播接收器的模板代码,说白了就是 registerReceiver 和 unregisterReceiver 这对难兄难弟。我们用高阶函数把它们封装成一个作用域函数。
inline fun Context.registerReceiver(
vararg actions: String,
crossinline onReceive: (context: Context, intent: Intent) -> Unit
): BroadcastReceiver {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
onReceive(context, intent)
}
}
val filter = IntentFilter().apply {
actions.forEach { addAction(it) }
}
registerReceiver(receiver, filter)
return receiver
}
inline fun Context.unregisterReceiverSafely(receiver: BroadcastReceiver?) {
try {
receiver?.let { unregisterReceiver(it) }
} catch (e: IllegalArgumentException) {
// 广播未注册时忽略
}
}
使用示例:
class MainActivity : AppCompatActivity() {
private var receiver: BroadcastReceiver? = null
override fun onResume() {
super.onResume()
receiver = registerReceiver(
Intent.ACTION_BATTERY_LOW,
Intent.ACTION_POWER_CONNECTED
) { _, intent ->
when (intent.action) {
Intent.ACTION_BATTERY_LOW -> showBatteryWarning()
Intent.ACTION_POWER_CONNECTED -> hideBatteryWarning()
}
}
}
override fun onPause() {
super.onPause()
unregisterReceiverSafely(receiver)
}
}
我曾经因为忘记在 onPause 里注销广播,导致 Activity 泄漏。后来我写了个 LifecycleObserver 配合这个高阶函数,自动在 onDestroy 时注销。嗯,这里就不展开了,但思路你可以自己试试。
13.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
13.6 避坑指南
- 渠道一旦创建,重要性等级不能修改。 只能删除 App 数据或卸载重装。所以上线前一定要确认好重要性等级。我有个朋友,上线后才发现 IM 通知用了 IMPORTANCE_MIN,用户收不到弹窗,被骂惨了。
- PendingIntent 的复用问题。 如果你用相同的 requestCode 和 Intent,系统会复用之前的 PendingIntent。这会导致通知点击后不更新数据。解决办法是加个随机数作为 requestCode。
- 广播接收器在 Android 8.0 之后的限制。 隐式广播大部分被限制了。如果你要监听系统广播,记得用 registerReceiver 动态注册,别在清单文件里静态注册了。
object ChannelIds { const val ORDER = "order" ... }。这样不会拼错字符串,也方便统一管理。
好了,这一章的内容就到这里。扩展函数和高阶函数在通知和广播中的应用,说白了就是「把重复的模板代码藏起来,把变化的逻辑露出来」。你写几次就会爱上这种写法。
公众号:蓝海资料掘金营,微信deep3321