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 一堆属性,那代码得多冗余。

核心思路: 用扩展函数给 NotificationManager 添加 DSL 风格的创建方法,用高阶函数接收配置闭包。这样渠道配置变成了声明式代码,可读性直接拉满。

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 列表就行,代码干净多了。

小技巧: 如果你的通知需要支持 Android 12 以上的通知 trampoline 限制,记得把 PendingIntent 的 flag 设为 FLAG_IMMUTABLE 或 FLAG_MUTABLE。我早期踩过这个坑,通知点击后直接崩溃。

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 时注销。嗯,这里就不展开了,但思路你可以自己试试。

注意: 如果你在 Fragment 中使用,记得在 onDestroyView 中注销,而不是 onDestroy。否则 View 相关的操作可能引发空指针。

13.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

扩展函数与高阶函数在通知与广播中的应用 通知渠道管理 Notification DSL BroadcastReceiver NotificationChannelBuilder 扩展函数 createChannel() NotificationBuilder 扩展函数 sendNotification() 高阶函数 registerReceiver() 安全注销 unregisterReceiverSafely() 核心收益 ✅ 减少模板代码,提升可读性 ✅ 统一管理渠道和通知配置 ✅ 避免广播注册泄漏,生命周期安全

13.6 避坑指南

  • 渠道一旦创建,重要性等级不能修改。 只能删除 App 数据或卸载重装。所以上线前一定要确认好重要性等级。我有个朋友,上线后才发现 IM 通知用了 IMPORTANCE_MIN,用户收不到弹窗,被骂惨了。
  • PendingIntent 的复用问题。 如果你用相同的 requestCode 和 Intent,系统会复用之前的 PendingIntent。这会导致通知点击后不更新数据。解决办法是加个随机数作为 requestCode。
  • 广播接收器在 Android 8.0 之后的限制。 隐式广播大部分被限制了。如果你要监听系统广播,记得用 registerReceiver 动态注册,别在清单文件里静态注册了。
我的习惯: 把所有的渠道 id 定义在一个 object 里,比如 object ChannelIds { const val ORDER = "order" ... }。这样不会拼错字符串,也方便统一管理。

好了,这一章的内容就到这里。扩展函数和高阶函数在通知和广播中的应用,说白了就是「把重复的模板代码藏起来,把变化的逻辑露出来」。你写几次就会爱上这种写法。


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