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

Android OkHttp/Retrofit:应用退至后台时如何续传多部分文件?

问题描述

我正在开发一款应用的文件上传功能,需求是通过一系列Multipart请求向上传端点提交一批文件,需显示每个文件的上传进度。文件可能较大且需在慢速网络下上传,因此采用依次上传的方式。

当应用在前台活跃时,功能表现符合预期,但当应用退至后台时,上传会暂停直至应用恢复,且过程中会触发重试,导致部分文件因多次挂起恢复而重复上传。请问该如何解决此问题?


Service.kt

@Multipart
@POST("upload/files")
suspend fun uploadFilesWithName(@Header("finalFile") finalFile: Boolean,
                                @Part file: MultipartBody.Part) : Response<FileUploadResponse>

Repository.kt

fun uploadFilesWithComplete(files: MutableList<MultipartBody.Part>): LiveData<ApiState<FileUploadResponse>?> = liveData {

    val counter = AtomicInteger()

    files.forEachIndexed { index, file ->

        submitFilesWithName(finalFile = (files.size - 1 == index), file = file)

            .onStart { emit(ApiState.loading()) }
            .collect { response ->
                counter.incrementAndGet()

                when(response) {
                    is ApiSuccessResponse<FileUploadResponse> -> {
                        emit(ApiState.success(data = response.data, position = index))
                    }

                    is ApiErrorResponse<FileUploadResponse> -> {
                        if (response.error != null) {
                            emit(
                                ApiState.error(
                                    error = response.error,
                                    msg = response.error.validation.summary,
                                    position = index
                                )
                            )
                        } else {
                            emit(
                                ApiState.error(
                                    error = null,
                                    msg = "Error uploading file.",
                                    position = index
                                )
                            )
                        }
                    }
                    else -> {}
                }
            }
    }
}

suspend fun submitFilesWithName(finalFile: Boolean = false,
                                file: MultipartBody.Part
) : Flow<ApiResponse<FileUploadResponse>?> {
    return flow {
        emit (
            ApiResponse.make(
                requestFunc = { service.uploadFilesWithName(
                    finalFile = finalFile,
                    file = file
                )},
                maxRetries = SHORT_RETRY_ATTEMPTS
            )
        )
    }.flowOn(Dispatchers.IO)
}

FileUploadDialog.kt

val files: MutableList<MultipartBody.Part>
... // Add files to the multipart body list
repository?.uploadFilesWithComplete(files = files)?.observe(viewLifecycleOwner) { response ->
    if (response != null) {
        when (response.status) {
            Status.LOADING -> { /* kick off a loading animation */ }
            Status.SUCCESS -> {
                response.position?.let { position ->
                    selectedFiles.value?.let { selected ->
                        selected[position].apply {
                            inError = false
                            errorMessage = ""
                            isComplete = true
                        }
                        binding.fileList.adapter?.notifyItemChanged(position)
                    }
                }
            }
            Status.ERROR -> {
                response.position?.let { position ->
                    selectedFiles.value?.let { selected ->
                        selected[position].apply {
                            inError = true
                            errorMessage = "Upload Failed"
                            isComplete = true
                        }
                        binding.fileList.adapter?.notifyItemChanged(position)
                    }
                }
            }
        }
    }
}

解决方案

1. 用前台服务维持后台上传进程

应用退到后台时,UI相关的协程和线程会被系统限制,导致上传中断。需要创建前台服务:

  • 前台服务会显示常驻通知,告知用户上传状态,系统不会轻易终止该进程。
  • 将上传逻辑从Repository的LiveData移到前台服务中,脱离UI生命周期的限制。

2. 取消自动重试,实现断点续传

当前自动重试机制会在上传挂起恢复后重新发起完整请求,导致重复上传。需要:

  • 关闭默认重试(maxRetries = 0),仅在明确的网络错误(如超时、连接失败)时手动重试。
  • 对接后端实现断点续传:上传时通过Content-Range头告知后端已上传的字节偏移量,后端接收并拼接文件内容。修改API接口添加该请求头。

3. 持久化上传状态,避免重复处理

用本地数据库(如Room)记录每个文件的上传状态:

  • 状态包括:未开始、上传中、已完成、上传失败。
  • 应用恢复前台时,读取本地状态,跳过已完成的文件,从上传中的断点处继续。

4. 优化协程作用域

当前使用viewLifecycleOwner观察LiveData,后台时LiveData会停止分发事件。改为在前台服务中使用独立的协程作用域:

private val uploadScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

该作用域不受UI生命周期影响,后台时仍能持续执行上传任务。


代码调整示例

前台服务核心逻辑

class UploadService : ForegroundService() {
    private val uploadScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private val UPLOAD_NOTIFICATION_ID = 1001

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 显示前台通知
        val notification = createUploadNotification()
        startForeground(UPLOAD_NOTIFICATION_ID, notification)

        // 获取待上传文件列表
        val files = intent?.getParcelableArrayListExtra<MultipartBody.Part>("files") ?: mutableListOf()
        uploadFiles(files)

        return START_STICKY
    }

    private fun uploadFiles(files: MutableList<MultipartBody.Part>) {
        uploadScope.launch {
            files.forEachIndexed { index, file ->
                // 从本地缓存读取已上传字节数
                val uploadedBytes = getUploadedBytesFromCache(index)
                val totalBytes = file.body.contentLength()

                if (uploadedBytes == totalBytes) {
                    // 文件已上传完成,跳过
                    updateFileStatus(index, Status.SUCCESS)
                    return@forEachIndexed
                }

                // 构建支持断点续传的请求
                val resumablePart = createResumablePart(file, uploadedBytes)
                val response = service.uploadFilesWithName(
                    finalFile = (files.size - 1 == index),
                    file = resumablePart
                )

                if (response.isSuccessful) {
                    updateFileStatus(index, Status.SUCCESS)
                    saveUploadedBytesToCache(index, totalBytes)
                } else {
                    // 仅在网络错误时重试
                    if (response.code() in 500..599 || response.code() == 408) {
                        // 手动重试逻辑
                    } else {
                        updateFileStatus(index, Status.ERROR)
                    }
                }
            }
            // 上传完成,停止前台服务
            stopForeground(STOP_FOREGROUND_REMOVE)
            stopSelf()
        }
    }

    // 辅助方法:创建通知、缓存上传进度、更新UI状态等
    private fun createUploadNotification(): Notification {
        // 实现通知构建逻辑
    }

    private fun getUploadedBytesFromCache(index: Int): Long {
        // 从本地数据库/SharedPreferences读取已上传字节数
    }

    private fun saveUploadedBytesToCache(index: Int, bytes: Long) {
        // 保存上传进度到本地
    }

    private fun updateFileStatus(index: Int, status: Status) {
        // 通过LiveData或广播通知UI更新状态
    }
}

修改Repository的重试逻辑

suspend fun submitFilesWithName(finalFile: Boolean = false,
                                file: MultipartBody.Part
) : Flow<ApiResponse<FileUploadResponse>?> {
    return flow {
        emit (
            ApiResponse.make(
                requestFunc = { service.uploadFilesWithName(
                    finalFile = finalFile,
                    file = file
                )},
                maxRetries = 0 // 关闭自动重试
            )
        )
    }.flowOn(Dispatchers.IO)
}

内容的提问来源于stack exchange,提问作者DevinM

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 06:35:37