提供的答案都不正确,这里有一个正确的解决方案,它正确地结合了两种方法。
首先为下载创建一个新的Retrofit对象,
此Retrofit对象不应包含任何日志拦截器,因为这会引发
java.lang.IllegalStateException: closed
异常。
fun interface ResponseBodyListener {
fun update(responseBody: ResponseBody)
}
fun getDownloaderRetrofit(listener: ResponseBodyListener): Retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.client(initHttpDownloadListenerClient(listener))
.build()
private fun initHttpDownloadListenerClient(listener: ResponseBodyListener): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.MINUTES)
.writeTimeout(0, TimeUnit.SECONDS)
.addNetworkInterceptor { chain ->
chain.proceed(chain.request()).also { originalResponse ->
originalResponse.body?.let { listener.update(it) }
}
}
.build()
}
在addNetworkInterceptor中,我们会尽快获取响应体,以便跟踪实际的下载进度。它包含从服务器下载的数据流。
以下是API接口
interface DownloadFilesApi {
@GET
@Streaming
suspend fun downloadFile(@Url url: String): ResponseBody
}
以下是请求本身
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun downloadFile(url: String, directory: File, fileName: String): Flow<DownloadFileState> =
callbackFlow {
val listener = ResponseBodyListener { responseBody: ResponseBody ->
this.launch {
responseBody.downloadToFileWithProgress(directory, fileName).collect {
trySend(it)
}
}
}
getDownloaderRetrofit(listener).create(DownloadFilesApi::class.java).downloadFile(url)
awaitClose()
}
请注意,downloadFile是挂起(coroutine)函数,这会暂停协程直到下载完成。这里的callbackFlow用作普通回调(callback)和flow结果之间的桥梁。
最后,downloadToFileWithProgress与@Robert编写的相同,区别在于此处显示文件下载进度而不是实际下载完成后写入桌面的进度。参考文件:RetrofitExtentions.kt。
fun ResponseBody.downloadToFileWithProgress(directory: File, filename: String): Flow<DownloadFileState> = flow {
emit(DownloadFileState.Progress(0))
var deleteFile = true
val file = File(directory, filename)
try {
byteStream().use { inputStream ->
file.outputStream().use { outputStream ->
val totalBytes = contentLength()
val data = ByteArray(8_192)
var progressBytes = 0L
while (true) {
val bytes = inputStream.read(data)
if (bytes == -1) {
break
}
outputStream.write(data, 0, bytes)
progressBytes += bytes
val progress = ((progressBytes * 100) / totalBytes).toInt()
emit(DownloadFileState.Progress(percent = progress))
}
when {
progressBytes < totalBytes ->
throw Exception("missing bytes")
progressBytes > totalBytes ->
throw Exception("too many bytes")
else ->
deleteFile = false
}
}
}
emit(DownloadFileState.Finished(file))
} finally {
if (deleteFile) {
file.delete()
}
}
}
为了完整起见,这里介绍如何获取您要保存视频的根文件夹。
fun videosRootFolder(context: Context): File {
return File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
APP_DIRECTORY_NAME
).run {
if (exists()) {
this
} else {
this.mkdir()
this
}
}
}