You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 01:33:24