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接口,这其实不太好……

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

  1. Android 通过蒲公英pgyer的接口 Service 实现带进度下载App 通知栏显示 在线更新 自动更新Demo

    Android 通过蒲公英pgyer的接口 Service 实现带进度下载App 通知栏显示 在线更新 自动更新Demo 标签: app在线更新下载Update升级 2016-09-18 20:47  ...

  2. Android网络库的比较:OkHTTP,Retrofit和Volley [关闭]

    本文翻译自:Comparison of Android networking libraries: OkHTTP, Retrofit, and Volley [closed] Two-part que ...

  3. android okhttpclient设置编码,Android之okhttp实现socket通讯(非原创)

    文章大纲 一.okhttp基础介绍二.socket通讯代码实战三.项目源码下载四.参考文章 一.okhttp基础介绍 二.socket通讯代码实战 1. 添加依赖和权限 app的build.gradl ...

  4. Android技能树 — 网络小结(6)之 OkHttp超超超超超超超详细解析

    前言: 本文也做了一次标题党,哈哈,其实写的还是很水,各位原谅我O(∩_∩)O. 介于自己的网络方面知识烂的一塌糊涂,所以准备写相关网络的文章,但是考虑全部写在一篇太长了,所以分开写,希望大家能仔细看 ...

  5. Android开发丶一步步教你实现okhttp带进度的列表下载文件功能

    大家好,我又回来了! 标题好像又起的不知所云,但是貌似也想不起更好的标题,看看效果图 现在有个文件列表,每个列表标签都有一个下载的按钮,点击以下载对应的文件,如果已下载则显示"已下载&quo ...

  6. Android中的Gradle之配置及构建优化

    一.Gradle简介 1.Gradle是什么? Gradle是一种项目自动化构建工具,基于Groovy语言来声明项目设置,同时支持kotlin文件xxx.gradle.kts作为DSL(Domain ...

  7. Android gradle统一依赖版本:Composing builds

    之前写过一篇Android gradle统一依赖版本:kotlin+buildSrc的集成使用, 两者的区别可以参照再见吧 buildSrc, 拥抱 Composing builds 提升 Andro ...

  8. android tv 云播放器,Android TV开发总结(六)构建一个TV app的直播节目实例

    近年来,Android TV的迅速发展,传统的有线电视受到较大的冲击,在TV上用户同样也可以看到各个有线电视的直播频道,相对于手机,这种直播节目,体验效果更佳,尤其是一样赛事节目,大屏幕看得才够痛快, ...

  9. android替换Glide通讯组件为Okhttp并监控加载进度,安卓rxjava获取网络时间

    import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; imp ...

  10. Android - 依赖统一管理

    前言 前段时间自己在搭建组件化框架时候遇到了多人协作 Moudle 版本依赖冲突以及重复导包和同一个包导入不同版本的情况,针对这个问题对依赖统一这块做了一次比较详细的学习和总结 目前Android依赖 ...

最新文章

  1. 如何学习数据挖掘和数据科学的7个步骤
  2. 【转】python包导入细节
  3. python3 selenium 无头浏览器 错误 FileNotFoundError: [Errno 2] No such file or directory: 'geckodriver'
  4. Learning to rank在淘宝的应用
  5. 第16天学习Java的笔记(标准类,Scanner)
  6. idea自定义快捷鍵
  7. cgi+bin+php,crontab+php-cgi/php 定时执行PHP脚本
  8. 2d访问冲突_Light | 基于环形分隔微镜阵列的高速随机访问轴向聚焦系统
  9. 把旧系统迁移到.Net Core 2.0 日记(2) - 依赖注入/日志NLog
  10. 洛谷找最小值c语言,洛谷 P1478 陶陶摘苹果(升级版) C语言实现
  11. Python异常捕获及自定义异常类
  12. java 泛型 类型形参(Type Parameters)Type Parameters 边界(Bound)
  13. 解决ScrollViewer嵌套的DataGrid、ListBox等控件的鼠标滚动事件无效
  14. 基于 libevent 开源框架实现的 web 服务器
  15. Java从入门到放弃系列
  16. 提升文学素养【文章解读】
  17. pycharm2017.3.3破解到2099年
  18. CSS---各种分割线
  19. 解决surface的幽灵触控
  20. StringTokenizer类详解

热门文章

  1. 计算机系统基础lab2(二进制炸弹实验)
  2. CH579 SPI WS2812B
  3. 【备忘】虚拟化容器/Docker视频教程/kubernetes/云计算/实例教程
  4. 西铁城手表最外圈数字是什么_手表外圈数字是什么意思 有什么作用
  5. Markdown用法——带圆圈的数字编号
  6. 在ARM板上移植CH341驱动
  7. YUV422转RGB并显示于Qlabel
  8. 【C语言】入门基础选择题附答案
  9. 数据线CE测试标准 准备资料
  10. maven打包常用命令总结