android http统一回调,Android使用OKHttp构建带进度回调的多文件下载器

title: Android使用OKHttp构建带进度回调的多文件下载器

date: 2018-09-29

categories: Android

tags: [Android,下载器,教程]

最近重构掌上重邮的教务新闻时遇到了一个问题:

如何制作一个支持同时下载多个文件,并且进行进度回调的下载器。

查阅并学习了一些资料后实现了需要的功能,在这里整理汇总

前言 && 避雷

本文主要介绍:如何使用OKHttp来构建带进度回调的文件下载器

本文适合对象:想要从构建的过程中学习操作的意义的Android开发者

本文例子均为Kotlin编写

思路

个人习惯,做事情之前先理清思路,大多数博客都没有关于思路的讲解,个人感觉Ctrl C+V和搬砖过于相似。

回调接口

分析需求(多文件下载,进度回调),很明显是一个类似应用商店的下载,那么我们回调的时候应该把每个回调分开进行传递。最简单的方法是每个下载传一个独特的接口进去;还有一种是给回调的每个方法加上id参数,使用同一个回调接口进行下载

监听进度

按照原生的写法,是在每次从网络流读入后记录读入的量,进行回调,那么只要在okhttp对应的位置进行修改,添加上回调就好

下载完成

完成后应该写入文件,此时进度回调应该是满的,但是下载完成的回调并没有调用,而是在完成写入文件后调用。

总结

流程:用户点击UI,选中多个下载。下载器接收请求url和监听器,给请求设置监听,让okhttp进行下载。根据id回调,统计下载结束的数量,写入文件完成后回调文件。

我认为这里应该分成UI(Activity)、数据控制器(ViewModel)、下载器(DownloadManager)、下载/写文件/打开文件

正文

下载器

回调接口

为了让下载器和需求的多下载解耦,我结合使用了前面提到的两种接口,从实现单下载入手,构建单文件下载的接口

import java.io.File

/**

* Author: Hosigus

* Date: 2018/9/23 18:06

* Description: 下载进度回调

*/

interface RedDownloadListener {

fun onDownloadStart()

fun onProgress(currentBytes: Long, contentLength: Long)

fun onSuccess(file: File)

fun onFail(e: Throwable)

}

监听OkHttp下载进度

要实现监听OkHttp的下载进度,我们需要从ResponseBody的fun source(): BufferedSource入手,以源的流作为真实的下载进度。

那我们重写ResponseBody,代码如下:

import okhttp3.ResponseBody

import okio.Buffer

import okio.BufferedSource

import okio.ForwardingSource

import okio.Okio

/**

* Author: Hosigus

* Date: 2018/9/23 18:08

* Description: 重写ForwardingSource的read方法,在read方法中计算百分比,回调进度

*/

class RedResponseBody(private val responseBody: ResponseBody,

private val listener: RedDownloadListener

) : ResponseBody() {

private val source by lazy {

Okio.buffer(

object : ForwardingSource(responseBody.source()) {

private var bytesRead = 0L

override fun read(sink: Buffer, byteCount: Long): Long {

val read = super.read(sink, byteCount)

if (read != -1L) {

bytesRead += read

listener.onProgress(bytesRead, responseBody.contentLength())

}

return read

}

}

)

}

override fun contentLength() = responseBody.contentLength()

override fun contentType() = responseBody.contentType()

override fun source(): BufferedSource = source

}

要将ResponseBody应用到OkHttp中,需要添加Interceptor

重写Interceptor,代码如下:

import okhttp3.Interceptor

import okhttp3.Response

/**

* Author: Hosigus

* Date: 2018/9/23 19:23

* Description: 将原ResponseBody拦截转换成RedResponseBody

*/

class RedDownloadInterceptor(private val listener: RedDownloadListener) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {

val response = chain.proceed(chain.request())

val body = response.body() ?: return response

return response.newBuilder().body(RedResponseBody(body, listener)).build()

}

}

最后调用addNetworkInterceptor方法,将Interceptor添加到OkHttp的Client中,就实现了带进度回调的下载器

Manager代码

下载器代码如下:

import android.os.Environment

import okhttp3.*

import java.io.File

import java.io.FileOutputStream

import java.io.IOException

import java.io.InputStream

/**

* Author: Hosigus

* Date: 2018/9/24 16:18

* Description: 下载的入口

*/

object DownloadManager {

fun download(listener: RedDownloadListener, url: String, fileName: String) {

val client = OkHttpClient.Builder()

.addNetworkInterceptor(RedDownloadInterceptor(listener))

.build()

listener.onDownloadStart()

client.newCall(Request.Builder().url(url).build())

.enqueue(object : retrofit2.Callback {

override fun onFailure(call: Call, t: Throwable) {

listener.onFail(t)

}

override fun onResponse(call: Call, response: Response) {

val body = response.body() ?: return

val state = Environment.getExternalStorageState()

if (Environment.MEDIA_MOUNTED != state && Environment.MEDIA_MOUNTED_READ_ONLY != state) {

listener.onFail(Exception("permission deny"))

return

}

val ins: InputStream

val fos: FileOutputStream

try {

ins = body.byteStream()

val file = File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),

"$fileName.${splitFileType(response.headers()["Content-Disposition"])}")

fos = FileOutputStream(file)

val bytes = ByteArray(1024)

var length = ins.read(bytes)

while (length != -1) {

fos.write(bytes, 0, length)

length = ins.read(bytes)

}

fos.flush()

listener.onSuccess(file)

} catch (e: Exception) {

listener.onFail(e)

}

}

})

}

}

注:其中关于文件的后缀,是由响应头中动态获取的

response.headers()["Content-Disposition"]?.let {

it.substring(it.indexOf("filename="), it.length).substringAfterLast(".")

}

更详细的内容请参考我的另一篇博客

其实到这里,本篇博客的标题内容已经结束了,之后的算作是后日谈,也算是使用实例,因为是为了解耦做了一定的操作。

控制器

回调接口

给UI的回调接口,根据UI改变的需要设计

interface NewsDownloadListener {

fun onDownloadStart()

fun onProgress(id: Int, currentBytes: Long, contentLength: Long)

fun onDownloadEnd(id: Int, file: File? = null, e: Throwable? = null)

}

控制下载

控制器接收确定的下载连接List,和监听器,进行下载。

当然,下载前需要进行权限检测,我这里使用了RxPermissions进行权限请求

最后下载代码如下:

fun download(rxPermissions: RxPermissions, list: List, listener: NewsDownloadListener) {

checkPermission(rxPermissions) { isGranted ->

if (isGranted) {

listener.onDownloadStart()

list.forEachIndexed { pos, it ->

DownloadManager.download(object : RedDownloadListener {

override fun onDownloadStart() {}

override fun onProgress(currentBytes: Long, contentLength: Long) {

listener.onProgress(pos, currentBytes, contentLength)

}

override fun onSuccess(file: File) {

listener.onDownloadEnd(pos, file)

}

override fun onFail(e: Throwable) {

listener.onDownloadEnd(pos, e = e)

}

}, it.url, it.name)

}

} else {

listener.onDownloadEnd(-1, e = Exception("permission deny"))

}

}

}

private fun checkPermission(rxPermissions: RxPermissions, result: (Boolean) -> Unit) {

rxPermissions.request(WRITE_EXTERNAL_STORAGE).subscribe(result).lifeCycle()

}

可以看到,控制器放弃了每次下载的onDownloadStart回调,而是在第一次下载开始前就回调UI下载开始;回调进度的时候添加上了id;合并了回调结果。

这都是为了UI做的中转变换,因为下载已经解耦了,所以可以按需求来进行控制层的接口变更,而不需要更改下载器的代码。

UI层

根据应用商店的排布,他需要独立管理下载完成的文件,因此我将下载的文件和数量均交给Listener管理

private val files = mutableListOf()

private var downloadNeedSize = 0

private var downloadEndSize = 0

当进行下载的时候,进行NeedSize的初始化

downloadNeedSize = list.size

viewModel.download(rxPermissions, list, this)

带ID的单文件下载完成回调

@Synchronized

override fun onDownloadEnd(id: Int, file: File?, e: Throwable?) {

if (file != null) {

files.add(file)

} else {

e?.printStackTrace()

AndroidSchedulers.mainThread().scheduleDirect {

...//UI提示相关错误

}

}

downloadEndSize++

if (downloadEndSize == downloadNeedSize) {

AndroidSchedulers.mainThread().scheduleDirect {

...//全部下载完成

}

}

}

另外俩回调就根据UI需求写了

写在最后

感觉功能并不复杂,使用Android原生也能实现,甚至改改DownloadManager就可以用了

但是就是不想那样做,可能是因为那样的做法写过了,想尝试一些别的操作

最开始尝试的是Retrofit+RxJava,之后发现过于麻烦,失去了使用他们的意义,最后还是决定从okhttp入手

然后是为了解耦合,将下载器和管理器分开了,虽然这样就多写了一层接口,但是我没有想到啥更好的解法

最后的问题就是懒得把进度管理和View再加一层隔开,是直接让Activity实现的NewsDownloadListener接口,这其实不太好……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Kotlin 中使用 OkHttp3 下载文件下载进度,可以通过以下步骤实现: 1. 添加 OkHttp3 依赖 在 app module 的 build.gradle 文件中添加以下代码: ``` dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' } ``` 2. 创建 OkHttp3 客户端 在代码中创建一个 OkHttpClient 客户端: ``` val client = OkHttpClient() ``` 3. 创建下载请求 使用 OkHttp3 的 Request.Builder 创建一个下载请求,并设置下载 URL 和保存文件的路径: ``` val request = Request.Builder() .url("https://example.com/file.zip") .build() ``` 4. 创建下载监听 定义一个回调接口,用于监听下载进度: ``` interface DownloadListener { fun onDownloadProgress(progress: Int) } ``` 在代码中实现这个接口,并在其中更新下载进度,例如: ``` val listener = object : DownloadListener { override fun onDownloadProgress(progress: Int) { runOnUiThread { // 更新下载进度条 progressBar.progress = progress } } } ``` 5. 发起下载请求 使用 OkHttpClient 的 newCall 方法发起下载请求,并在 enqueue 方法中传入一个 Callback 参数,该参数将在下载完成时回调: ``` client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // 下载失败 } override fun onResponse(call: Call, response: Response) { val inputStream: InputStream = response.body?.byteStream() ?: return // 保存文件并更新下载进度 val totalSize: Long = response.body?.contentLength() ?: -1 var downloadedSize: Long = 0 val outputStream = FileOutputStream("/storage/emulated/0/Download/file.zip") val buffer = ByteArray(8192) while (true) { val bytes = inputStream.read(buffer) if (bytes == -1) break outputStream.write(buffer, 0, bytes) downloadedSize += bytes val progress = (downloadedSize * 100 / totalSize).toInt() listener.onDownloadProgress(progress) } outputStream.close() inputStream.close() // 下载完成 } }) ``` 这段代码中,我们首先从 response.body 中获取输入流并创建输出流,然后使用循环逐段读取输入流的数据,再将其写入输出流,并计算下载进度,最后调用 DownloadListener 的 onDownloadProgress 方法更新下载进度。在下载完成后,我们需要关闭输入流和输出流,以及在 onFailure 方法中处理下载失败的情况。 6. 完整代码 最终的代码应该类似于这样: ``` interface DownloadListener { fun onDownloadProgress(progress: Int) } val client = OkHttpClient() val request = Request.Builder() .url("https://example.com/file.zip") .build() val listener = object : DownloadListener { override fun onDownloadProgress(progress: Int) { runOnUiThread { // 更新下载进度条 progressBar.progress = progress } } } client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // 下载失败 } override fun onResponse(call: Call, response: Response) { val inputStream: InputStream = response.body?.byteStream() ?: return // 保存文件并更新下载进度 val totalSize: Long = response.body?.contentLength() ?: -1 var downloadedSize: Long = 0 val outputStream = FileOutputStream("/storage/emulated/0/Download/file.zip") val buffer = ByteArray(8192) while (true) { val bytes = inputStream.read(buffer) if (bytes == -1) break outputStream.write(buffer, 0, bytes) downloadedSize += bytes val progress = (downloadedSize * 100 / totalSize).toInt() listener.onDownloadProgress(progress) } outputStream.close() inputStream.close() // 下载完成 } }) ``` 注意,这段代码中保存文件的路径是硬编码的,你需要根据实际需求修改它。另外,为了更新 UI,我们需要在 onDownloadProgress 方法中使用 runOnUiThread 方法,以确保在主线程中执行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值