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接口,这其实不太好……
android http统一回调,Android使用OKHttp构建带进度回调的多文件下载器相关推荐
- Android 通过蒲公英pgyer的接口 Service 实现带进度下载App 通知栏显示 在线更新 自动更新Demo
Android 通过蒲公英pgyer的接口 Service 实现带进度下载App 通知栏显示 在线更新 自动更新Demo 标签: app在线更新下载Update升级 2016-09-18 20:47 ...
- Android网络库的比较:OkHTTP,Retrofit和Volley [关闭]
本文翻译自:Comparison of Android networking libraries: OkHTTP, Retrofit, and Volley [closed] Two-part que ...
- android okhttpclient设置编码,Android之okhttp实现socket通讯(非原创)
文章大纲 一.okhttp基础介绍二.socket通讯代码实战三.项目源码下载四.参考文章 一.okhttp基础介绍 二.socket通讯代码实战 1. 添加依赖和权限 app的build.gradl ...
- Android技能树 — 网络小结(6)之 OkHttp超超超超超超超详细解析
前言: 本文也做了一次标题党,哈哈,其实写的还是很水,各位原谅我O(∩_∩)O. 介于自己的网络方面知识烂的一塌糊涂,所以准备写相关网络的文章,但是考虑全部写在一篇太长了,所以分开写,希望大家能仔细看 ...
- Android开发丶一步步教你实现okhttp带进度的列表下载文件功能
大家好,我又回来了! 标题好像又起的不知所云,但是貌似也想不起更好的标题,看看效果图 现在有个文件列表,每个列表标签都有一个下载的按钮,点击以下载对应的文件,如果已下载则显示"已下载&quo ...
- Android中的Gradle之配置及构建优化
一.Gradle简介 1.Gradle是什么? Gradle是一种项目自动化构建工具,基于Groovy语言来声明项目设置,同时支持kotlin文件xxx.gradle.kts作为DSL(Domain ...
- Android gradle统一依赖版本:Composing builds
之前写过一篇Android gradle统一依赖版本:kotlin+buildSrc的集成使用, 两者的区别可以参照再见吧 buildSrc, 拥抱 Composing builds 提升 Andro ...
- android tv 云播放器,Android TV开发总结(六)构建一个TV app的直播节目实例
近年来,Android TV的迅速发展,传统的有线电视受到较大的冲击,在TV上用户同样也可以看到各个有线电视的直播频道,相对于手机,这种直播节目,体验效果更佳,尤其是一样赛事节目,大屏幕看得才够痛快, ...
- android替换Glide通讯组件为Okhttp并监控加载进度,安卓rxjava获取网络时间
import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; imp ...
- Android - 依赖统一管理
前言 前段时间自己在搭建组件化框架时候遇到了多人协作 Moudle 版本依赖冲突以及重复导包和同一个包导入不同版本的情况,针对这个问题对依赖统一这块做了一次比较详细的学习和总结 目前Android依赖 ...
最新文章
- 如何学习数据挖掘和数据科学的7个步骤
- 【转】python包导入细节
- python3 selenium 无头浏览器 错误 FileNotFoundError: [Errno 2] No such file or directory: 'geckodriver'
- Learning to rank在淘宝的应用
- 第16天学习Java的笔记(标准类,Scanner)
- idea自定义快捷鍵
- cgi+bin+php,crontab+php-cgi/php 定时执行PHP脚本
- 2d访问冲突_Light | 基于环形分隔微镜阵列的高速随机访问轴向聚焦系统
- 把旧系统迁移到.Net Core 2.0 日记(2) - 依赖注入/日志NLog
- 洛谷找最小值c语言,洛谷 P1478 陶陶摘苹果(升级版) C语言实现
- Python异常捕获及自定义异常类
- java 泛型 类型形参(Type Parameters)Type Parameters 边界(Bound)
- 解决ScrollViewer嵌套的DataGrid、ListBox等控件的鼠标滚动事件无效
- 基于 libevent 开源框架实现的 web 服务器
- Java从入门到放弃系列
- 提升文学素养【文章解读】
- pycharm2017.3.3破解到2099年
- CSS---各种分割线
- 解决surface的幽灵触控
- StringTokenizer类详解