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
相关产品推荐
相关产品推荐

