基于WorkManager实现符合特定需求的Android多文件下载方案咨询
基于WorkManager实现符合要求的文件下载方案
核心思路
用Room持久化下载任务队列 + 单一CoroutineWorker串行处理任务,通过数据库作为UI与Worker的通信桥梁,既解决动态增删任务的问题,又规避静态变量的不良实践,同时满足Android12+的后台运行限制。
方案细节
1. 任务队列持久化(Room实现)
创建Room实体类存储下载任务的全量状态,确保任务在App重启、进程被杀后不丢失:
@Entity(tableName = "download_tasks") data class DownloadTask( @PrimaryKey val url: String, // 用URL作为唯一标识 val fileName: String, @ColumnInfo(defaultValue = "0") var status: Int, // 0:等待,1:下载中,2:已完成,3:已取消,4:失败 @ColumnInfo(defaultValue = "0") var progress: Int, val savePath: String ) @Dao interface DownloadTaskDao { @Query("SELECT * FROM download_tasks WHERE status IN (0,1) ORDER BY rowid ASC") fun getActiveTasks(): Flow<List<DownloadTask>> // 监听待执行/执行中的任务 @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun addTask(task: DownloadTask) @Update suspend fun updateTask(task: DownloadTask) @Query("UPDATE download_tasks SET status = 3 WHERE url = :url") suspend fun cancelTask(url: String) }
2. 单一CoroutineWorker实现串行下载
创建唯一的Worker实例,通过监听Room的任务队列变化,串行处理每个下载任务,同时维护前台通知:
class DownloadWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { private val notificationManager = context.getSystemService(NotificationManager::class.java) private val taskDao = AppDatabase.getInstance(context).downloadTaskDao() private val notificationId = 1001 private lateinit var notificationBuilder: NotificationCompat.Builder override suspend fun doWork(): Result { // 初始化前台通知,WorkManager自动处理Android12+后台启动限制 initForegroundNotification() // 监听任务队列变化,串行处理任务 taskDao.getActiveTasks().collect { tasks -> val pendingTasks = tasks.filter { it.status == 0 } val currentTask = tasks.firstOrNull { it.status == 1 } ?: pendingTasks.firstOrNull() currentTask?.let { task -> // 更新任务状态为下载中 taskDao.updateTask(task.copy(status = 1)) try { // 执行下载逻辑,替换为你的实际下载实现(如OkHttp) downloadFile(task) { progress -> // 更新任务进度到数据库 taskDao.updateTask(task.copy(progress = progress)) // 更新通知进度 updateNotificationProgress(task, progress) // 检查任务是否被取消 if (isStopped || taskDao.getTaskById(task.url).status == 3) { throw CancellationException("Task cancelled") } } // 下载完成,更新状态 taskDao.updateTask(task.copy(status = 2)) updateNotificationComplete(task) } catch (e: Exception) { // 处理下载失败或取消 val newStatus = if (isStopped || e is CancellationException) 3 else 4 taskDao.updateTask(task.copy(status = newStatus)) updateNotificationFailedOrCancelled(task, newStatus) } } // 无待处理任务时,终止Worker并取消通知 if (tasks.isEmpty()) { notificationManager.cancel(notificationId) return@collect } } return Result.success() } private fun initForegroundNotification() { val channelId = "download_channel" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( channelId, "下载通知", NotificationManager.IMPORTANCE_LOW ) notificationManager.createNotificationChannel(channel) } notificationBuilder = NotificationCompat.Builder(applicationContext, channelId) .setSmallIcon(R.drawable.ic_download) .setContentTitle("下载任务队列") .setContentText("等待下载...") .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) .setProgress(100, 0, true) startForeground(notificationId, notificationBuilder.build()) } private fun updateNotificationProgress(task: DownloadTask, progress: Int) { notificationBuilder.apply { setContentTitle("正在下载: ${task.fileName}") setContentText("进度: $progress%") setProgress(100, progress, false) } notificationManager.notify(notificationId, notificationBuilder.build()) } private fun updateNotificationComplete(task: DownloadTask) { notificationBuilder.apply { setContentTitle("下载完成: ${task.fileName}") setContentText("点击查看文件") setProgress(0, 0, false) setOngoing(false) // 可选:添加打开文件的PendingIntent val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(Uri.fromFile(File(task.savePath)), "application/octet-stream") flags = Intent.FLAG_ACTIVITY_NEW_TASK } setContentIntent(PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)) } notificationManager.notify(notificationId, notificationBuilder.build()) } private fun updateNotificationFailedOrCancelled(task: DownloadTask, status: Int) { val title = if (status == 3) "下载已取消: ${task.fileName}" else "下载失败: ${task.fileName}" notificationBuilder.apply { setContentTitle(title) setContentText("点击重试") setProgress(0, 0, false) setOngoing(false) // 可选:添加重试按钮的PendingIntent val retryIntent = Intent(applicationContext, DownloadRetryReceiver::class.java).apply { putExtra("url", task.url) } addAction(R.drawable.ic_retry, "重试", PendingIntent.getBroadcast(applicationContext, task.url.hashCode(), retryIntent, PendingIntent.FLAG_IMMUTABLE)) } notificationManager.notify(notificationId, notificationBuilder.build()) } // 示例下载方法,替换为你的实际下载逻辑 private suspend fun downloadFile(task: DownloadTask, onProgress: (Int) -> Unit) { repeat(100) { delay(50) onProgress(it + 1) if (isStopped) throw CancellationException() } } }
3. UI层与Worker的交互
- 添加下载任务:直接向Room插入任务,同时确保Worker已启动:
suspend fun addDownloadTask(url: String, fileName: String, savePath: String) { val task = DownloadTask(url, fileName, 0, 0, savePath) AppDatabase.getInstance(context).downloadTaskDao().addTask(task) // 用UniqueWork保证仅存在一个下载Worker实例 WorkManager.getInstance(context).enqueueUniqueWork( "download_worker", ExistingWorkPolicy.KEEP, OneTimeWorkRequestBuilder<DownloadWorker>().build() ) }
- 取消下载任务:更新Room中对应任务的状态,Worker会自动终止任务:
suspend fun cancelDownloadTask(url: String) { AppDatabase.getInstance(context).downloadTaskDao().cancelTask(url) }
解决原方案的痛点
- 动态增删任务:通过Room数据库作为通信媒介,UI和Worker无需直接交互,规避静态变量的不良实践。
- 串行下载:Worker从队列中逐个取出任务执行,确保同一时间仅下载一个文件。
- 后台调度:WorkManager自动适配Android12+的后台启动限制,Worker的
startForeground()合法有效,任务队列持久化保证进程被杀后可恢复。 - 通知更新:使用固定ID的通知,仅更新内容而非频繁创建/销毁,支持进度展示和取消操作(取消按钮可绑定PendingIntent触发任务状态更新)。
关键WorkManager特性利用
- Unique Work:通过
enqueueUniqueWork确保全局仅存在一个下载Worker实例,避免重复执行。 - CoroutineWorker:结合Flow监听数据库变化,异步处理下载任务,代码更简洁易维护。
- startForeground():WorkManager在API31+中自动处理前台服务的启动权限,无需额外适配。
内容的提问来源于stack exchange,提问作者algrid
相关产品推荐
相关产品推荐

