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_LOWIMPORTANCE_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开始,提供了ListenableWorkerForegroundService的支持。说白了,就是让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与通知结合的整体架构,我画了一张图:

WorkManager与通知结合架构图 WorkManager Worker / CoroutineWorker / ListenableWorker 普通Worker doWork()中发通知 前台服务Worker setForeground() 长时间运行Worker 定期更新通知 Notification(通知渠道 + 进度条 + 持续标志) Result.success / failure

20.5 避坑指南

最后,分享几个我踩过的坑:

  • 通知渠道重要性不能改:一旦发布应用,渠道的重要性级别就固定了。想改?只能卸载重装。所以,上线前想清楚。
  • 前台服务通知必须显示:Android系统要求前台服务必须有一个持续通知。如果你试图取消这个通知,系统会强制重新显示。我曾经试过在任务完成后取消通知,结果通知闪了一下又出现了。
  • setForeground的调用时机:必须在doWork开始后尽快调用。如果延迟太久,系统会认为Worker没有及时启动前台服务,直接抛出IllegalStateException
  • 通知ID不要重复:如果你在多个Worker中使用相同的通知ID,通知会互相覆盖。我习惯用任务ID或随机数作为通知ID,确保唯一性。

个人建议:对于需要用户感知的任务(如下载、上传),使用前台服务Worker。对于不需要用户感知的任务(如数据同步),使用普通Worker,只在任务失败时发通知。这样既不会打扰用户,又能保证任务可靠执行。

好了,关于WorkManager与通知的结合,就讲到这里。记住,通知是用户感知后台任务的窗口,用好它,你的应用会显得更专业、更可靠。


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