本文已参与「新人创作礼」活动,一起开启掘金创作之路。
这篇文章我们使用 Flow + Retrofit 实现一个文件下载的功能。
一、声明 ApiService 接口
使用 Retrofit 需要定义一个接口:
interface DownloadService {
@Streaming
@GET
fun download(@Url url: String): Call<ResponseBody>
}
Retrofit 下载文件时,默认会存储到缓存中,如果要下载大文件,需要用 @Streaming
将返回数据转换成流。
二、定义 DownloadState
定义一些记录下载状态的类:
sealed class DownloadState {
data class InProgress(val progress: Int) : DownloadState()
data class Success(val file: File) : DownloadState()
data class Error(val throwable: Throwable) : DownloadState()
}
InProgress 用于回调下载进度,Success 表示下载成功,Error 表示下载失败。
这里让他们都继承自 DownloadState,待会构建 Flow 时,就可以使用 DownloadState 作为流中传输的对象类型。
三、定义 DownloadManager
在 DownloadManager 中实现下载功能:
object DownloadManager {
suspend fun download(url: String, file: File): Flow<DownloadState> {
return flow {
val retrofit = Retrofit.Builder()
.baseUrl(UrlUtils.getBaseUrl(url))
.build()
val response = retrofit.create(DownloadService::class.java).download(url).execute()
if (response.isSuccessful) {
saveToFile(response.body()!!, file) {
emit(DownloadState.InProgress(it))
}
emit(DownloadState.Success(file))
} else {
emit(DownloadState.Error(IOException(response.toString())))
}
}.catch {
emit(DownloadState.Error(it))
}.flowOn(Dispatchers.IO)
}
private inline fun saveToFile(responseBody: ResponseBody, file: File, progressListener: (Int) -> Unit) {
val total = responseBody.contentLength()
var bytesCopied = 0
var emittedProgress = 0
file.outputStream().use { output ->
val input = responseBody.byteStream()
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = input.read(buffer)
while (bytes >= 0) {
output.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = input.read(buffer)
val progress = (bytesCopied * 100 / total).toInt()
if (progress - emittedProgress > 0) {
progressListener(progress)
emittedProgress = progress
}
}
}
}
}
其中,用到的 UrlUtils 类如下:
object UrlUtils {
/**
* 从url分割出BaseUrl
*/
fun getBaseUrl(url: String): String {
var mutableUrl = url
var head = ""
var index = mutableUrl.indexOf("://")
if (index != -1) {
head = mutableUrl.substring(0, index + 3)
mutableUrl = mutableUrl.substring(index + 3)
}
index = mutableUrl.indexOf("/")
if (index != -1) {
mutableUrl = mutableUrl.substring(0, index + 1)
}
return head + mutableUrl
}
}
这个工具用于从 url 中分割出 baseUrl。
在 download 方法中,我们构建了一个 Flow<DownloadState>
对象,使用 retrofit 的 execute 方法拿到 Response 对象,然后通过 saveToFile
方法将返回的 ResponseBody 写入文件中。
在写入过程中,当进度发生改变后,发射 DownloadState.InProgress 通知客户端。
下载完成后,发射 DownloadState.Success。
下载出现错误时,发射 DownloadState.Error。下载出错的地方包括:
- response.isSuccessful 为 false。
- response.body() 为 null,此时 response.body()!! 会抛出空指针异常,被 flow 的 catch 捕获到。
- 网络请求中发生的其他异常,被 flow 的 catch 捕获到。
最后,通过 flowOn 使其在 IO 线程中运行。
四、使用 DownloadManager
在 Activity 或者 Fragment 中使用 DownloadManager:
lifecycleScope.launchWhenCreated {
DownloadManager.download(
"https://upload-images.jianshu.io/upload_images/5809200-a99419bb94924e6d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240",
File(getExternalFilesDir(""), "image.png")
).collect {
when (it) {
is DownloadState.InProgress -> {
Log.d("~~~", "download in progress: ${it.progress}.")
}
is DownloadState.Success -> {
Log.d("~~~", "download finished.")
}
is DownloadState.Error -> {
Log.d("~~~", "download error: ${it.throwable}.")
}
}
}
}
这里我们采用 lifecycleScope 作为协程作用域,然后使用 DownloadManager 下载一张图片,保存到应用的缓存文件夹下。
这里调用 collect() 收集数据时,是在应用的主线程中,所以可以做一些 UI 相关的操作。本例中只是打印了一些日志。
运行程序,Log 控制台输出如下:
~~~: download in progress: 11.
~~~: download in progress: 23.
~~~: download in progress: 35.
~~~: download in progress: 47.
~~~: download in progress: 59.
~~~: download in progress: 71.
~~~: download in progress: 83.
~~~: download in progress: 95.
~~~: download in progress: 100.
~~~: download finished.
五、小结
下载文件很适合使用 Flow,通过 flowOn 切换到 IO 线程下载,然后在主线程中收集,非常方便。