20、WorkManager与通知结合:在Worker中发送通知、创建通知渠道、前台服务与WorkManager、长时间运行任务的通知展示
通知,是Android应用与用户沟通的重要桥梁。而WorkManager,则是后台任务的调度大师。当这两者结合,就能让我们的应用在后台默默完成任务后,及时给用户一个反馈。
说实话,我在早期做后台任务时,经常遇到一个问题:任务跑完了,用户完全不知道。或者任务正在跑,用户以为卡死了。嗯,今天我们就来解决这个问题。
20.1 通知渠道:Android 8.0的必修课
从Android 8.0(API 26)开始,Google强制要求所有通知必须绑定通知渠道。否则,通知根本发不出去。我当年第一次适配8.0时,就踩了这个坑——测试机死活不弹通知,查了半天才发现是渠道没创建。
核心要点:通知渠道一旦创建,就不能修改名称和重要性级别。只能删除应用重新安装。所以,命名要想清楚。
创建通知渠道的代码,我习惯放在Application的onCreate里,或者一个懒加载的单例中:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
private fun createNotificationChannel() {
val channelId = "work_manager_channel"
val channelName = "后台任务通知"
val importance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(channelId, channelName, importance).apply {
description = "用于展示WorkManager后台任务的执行状态"
enableVibration(true)
setShowBadge(true)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
这里有个细节:IMPORTANCE_HIGH会让通知弹出并伴有声音。如果你只是想让用户知道任务完成了,不想打扰他,用IMPORTANCE_LOW或IMPORTANCE_DEFAULT会更合适。
20.2 在Worker中发送通知
Worker运行在后台线程,但它可以访问Application的Context。所以,发通知完全没问题。
我曾经在做一个文件上传功能时,需要在Worker里通知用户上传进度。一开始我直接用NotificationManager.notify(),结果发现通知ID重复了,导致进度条不更新。后来我改用任务ID作为通知ID,才解决。
class UploadWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
val notificationManager = getNotificationManager()
val notification = createNotification("文件上传中...", 0)
notificationManager.notify(1001, notification)
// 模拟上传过程
for (i in 1..100) {
Thread.sleep(100)
val progressNotification = createNotification("上传进度: $i%", i)
notificationManager.notify(1001, progressNotification)
}
val doneNotification = createNotification("上传完成!", 100)
notificationManager.notify(1001, doneNotification)
return Result.success()
}
private fun createNotification(content: String, progress: Int): Notification {
val channelId = "work_manager_channel"
return NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(android.R.drawable.ic_menu_upload)
.setContentTitle("文件上传")
.setContentText(content)
.setProgress(100, progress, false)
.setOngoing(progress < 100)
.build()
}
private fun getNotificationManager(): NotificationManager {
return applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
}
小技巧:使用setOngoing(true)可以让通知不可滑动清除,适合展示正在进行的任务。任务完成后,记得设为false或直接取消通知。
20.3 前台服务与WorkManager:长时间运行任务的正确姿势
Android系统对后台任务的限制越来越严格。如果你的任务可能运行超过10分钟,比如下载大文件、处理视频,那么普通的Worker可能会被系统杀掉。
这时候,就需要前台服务出场了。WorkManager从2.3.0开始,提供了ListenableWorker和ForegroundService的支持。说白了,就是让Worker能以前台服务的方式运行,同时显示一个持续通知。
我记得有一次,用户反馈说上传大视频时,锁屏后再打开,上传就断了。排查后发现,系统在后台限制了Worker的执行时间。改用前台服务后,问题解决。
20.3.1 使用setForegroundAsync
对于CoroutineWorker,我们可以用setForeground方法:
class VideoProcessWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 创建前台通知
val notification = createForegroundNotification()
setForeground(ForegroundInfo(2001, notification))
// 执行长时间任务
return processVideo()
}
private fun createForegroundNotification(): Notification {
val channelId = "work_manager_channel"
return NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(android.R.drawable.ic_menu_edit)
.setContentTitle("视频处理中")
.setContentText("请勿关闭应用")
.setOngoing(true)
.build()
}
private suspend fun processVideo(): Result {
// 模拟视频处理
delay(5000)
return Result.success()
}
}
注意:调用setForeground必须在doWork开始后的合理时间内完成,否则系统会认为Worker没有及时启动前台服务,从而抛出异常。我个人建议在doWork的第一行就调用。
20.3.2 长时间运行任务的通知展示
对于真正长时间运行的任务,比如下载几百兆的文件,我们需要一个更精细的通知展示方案。我一般会这样做:
- 任务开始:显示一个持续通知,带进度条
- 任务进行中:定期更新进度,让用户知道没卡死
- 任务完成:更新通知为完成状态,取消持续标志
- 任务失败:显示错误通知,让用户知道发生了什么
class DownloadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val notificationManager = getNotificationManager()
val notificationId = 3001
// 启动前台服务
val startNotification = createNotification("准备下载...", 0, true)
setForeground(ForegroundInfo(notificationId, startNotification))
return try {
// 模拟下载,每10%更新一次通知
for (i in 1..10) {
delay(2000)
val progress = i * 10
val progressNotification = createNotification(
"下载进度: $progress%",
progress,
true
)
notificationManager.notify(notificationId, progressNotification)
}
// 下载完成
val doneNotification = createNotification("下载完成", 100, false)
notificationManager.notify(notificationId, doneNotification)
Result.success()
} catch (e: Exception) {
val errorNotification = createNotification("下载失败: ${e.message}", 0, false)
notificationManager.notify(notificationId, errorNotification)
Result.failure()
}
}
private fun createNotification(
content: String,
progress: Int,
isOngoing: Boolean
): Notification {
val channelId = "work_manager_channel"
return NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(android.R.drawable.ic_menu_download)
.setContentTitle("文件下载")
.setContentText(content)
.setProgress(100, progress, false)
.setOngoing(isOngoing)
.build()
}
private fun getNotificationManager(): NotificationManager {
return applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
}
20.4 知识体系总览
为了让你更直观地理解WorkManager与通知结合的整体架构,我画了一张图:
20.5 避坑指南
最后,分享几个我踩过的坑:
- 通知渠道重要性不能改:一旦发布应用,渠道的重要性级别就固定了。想改?只能卸载重装。所以,上线前想清楚。
- 前台服务通知必须显示:Android系统要求前台服务必须有一个持续通知。如果你试图取消这个通知,系统会强制重新显示。我曾经试过在任务完成后取消通知,结果通知闪了一下又出现了。
- setForeground的调用时机:必须在
doWork开始后尽快调用。如果延迟太久,系统会认为Worker没有及时启动前台服务,直接抛出IllegalStateException。 - 通知ID不要重复:如果你在多个Worker中使用相同的通知ID,通知会互相覆盖。我习惯用任务ID或随机数作为通知ID,确保唯一性。
个人建议:对于需要用户感知的任务(如下载、上传),使用前台服务Worker。对于不需要用户感知的任务(如数据同步),使用普通Worker,只在任务失败时发通知。这样既不会打扰用户,又能保证任务可靠执行。
好了,关于WorkManager与通知的结合,就讲到这里。记住,通知是用户感知后台任务的窗口,用好它,你的应用会显得更专业、更可靠。
公众号:蓝海资料掘金营,微信deep3321