如何通过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
相关产品推荐
相关产品推荐

