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

如何通过Retrofit coroutine API调用实现文件下载进度显示?

在Kotlin协程中用Retrofit实现带进度的文件下载

嗨,这个需求我刚好折腾过好几次,其实核心就是给Retrofit的下载请求加上进度监听,同时适配协程的挂起特性,下面我一步步给你讲清楚怎么实现:

1. 自定义带进度监听的ResponseBody

Retrofit默认的ResponseBody不会返回下载进度,所以得自己包装一个。这里我们用Okio的ForwardingSource拦截流的读取操作,计算已读字节数和总大小,再通过回调把进度抛出去:

interface ProgressListener {
    fun onProgress(currentBytes: Long, totalBytes: Long)
}

class ProgressResponseBody(
    private val originalResponseBody: ResponseBody,
    private val progressListener: ProgressListener
) : ResponseBody() {

    override fun contentType() = originalResponseBody.contentType()

    override fun contentLength() = originalResponseBody.contentLength()

    override fun source(): BufferedSource {
        return originalResponseBody.source()
            .let { source ->
                object : ForwardingSource(source) {
                    private var totalRead = 0L

                    override fun read(sink: Buffer, byteCount: Long): Long {
                        val bytesRead = super.read(sink, byteCount)
                        totalRead += if (bytesRead != -1L) bytesRead else 0L
                        progressListener.onProgress(totalRead, contentLength())
                        return bytesRead
                    }
                }
            }
            .buffer()
    }
}

2. 给Retrofit添加进度拦截器

接下来要把自定义的ResponseBody集成到Retrofit里,写一个Interceptor替换响应里的原始ResponseBody:

class ProgressInterceptor(private val progressListener: ProgressListener) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalResponse = chain.proceed(chain.request())
        return originalResponse.newBuilder()
            .body(ProgressResponseBody(originalResponse.body()!!, progressListener))
            .build()
    }
}

然后构建Retrofit时加入这个拦截器,别忘了用协程的CallAdapter:

val retrofit = Retrofit.Builder()
    .baseUrl("你的API基础地址")
    .client(OkHttpClient.Builder()
        // 这里先占位,后面在协程里动态传入带UI更新的监听
        .addInterceptor(ProgressInterceptor(object : ProgressListener {
            override fun onProgress(currentBytes: Long, totalBytes: Long) {}
        }))
        .build())
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(CoroutineCallAdapterFactory())
    .build()

3. 定义协程风格的API接口

和普通下载接口一样,用suspend函数返回ResponseBody,必须加@Streaming注解避免大文件加载到内存:

interface DownloadApi {
    @GET
    @Streaming
    suspend fun downloadFile(@Url url: String): ResponseBody
}

4. 在协程中调用并处理进度更新

最后在协程作用域(比如ViewModelScope、LifecycleScope)里发起请求,写入本地文件的同时更新UI:

// 以ViewModel为例
class DownloadViewModel : ViewModel() {
    val downloadProgress = MutableStateFlow(0)
    private val downloadApi = retrofit.create(DownloadApi::class.java)

    fun startDownload(fileUrl: String, savePath: String) {
        viewModelScope.launch {
            try {
                val progressListener = object : ProgressListener {
                    override fun onProgress(currentBytes: Long, totalBytes: Long) {
                        if (totalBytes > 0) {
                            val progress = (currentBytes * 100 / totalBytes).toInt()
                            // 用StateFlow把进度发送到UI层
                            downloadProgress.value = progress
                        }
                    }
                }

                // 重新创建带当前监听的OkHttpClient(或者用更灵活的方式传递监听)
                val tempClient = OkHttpClient.Builder()
                    .addInterceptor(ProgressInterceptor(progressListener))
                    .build()
                val tempRetrofit = retrofit.newBuilder().client(tempClient).build()
                val tempApi = tempRetrofit.create(DownloadApi::class.java)

                val responseBody = tempApi.downloadFile(fileUrl)
                val saveFile = File(savePath)

                // 写入文件,use函数自动关闭流
                responseBody.byteStream().use { input ->
                    saveFile.outputStream().use { output ->
                        input.copyTo(output)
                    }
                }

                // 下载完成后更新进度为100
                downloadProgress.value = 100
            } catch (e: Exception) {
                // 处理异常,比如网络错误、文件写入失败
                e.printStackTrace()
            }
        }
    }
}

关键细节提醒

  • @Streaming注解不能忘:大文件下载不加这个,Retrofit会把整个文件加载到内存,极易引发OOM。
  • 线程切换:进度回调默认在OkHttp的子线程,用StateFlow或者withContext(Dispatchers.Main)切换到主线程更新UI。
  • 处理总大小为-1的情况:部分服务器不返回Content-Length头,此时可以显示“正在下载”而非百分比。
  • 资源释放:用use函数自动关闭输入输出流,避免资源泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:36:56