OkHttp初探2:如何使用OkHttp进行下载封装?带进度条?Kotlin+Flow版本。

Posted pumpkin的玄学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OkHttp初探2:如何使用OkHttp进行下载封装?带进度条?Kotlin+Flow版本。相关的知识,希望对你有一定的参考价值。

本文接上一篇博文:OkHttp初探:如何使用OkHttp进行Get或Post请求?Kotlin版本。

通用模块封装

这里封装一些通用的代码,先知道一下就可以了。

/**
 * 日志打印
 */
fun log(vararg msg: Any?) 
    val nowTime = SimpleDateFormat("HH:mm:ss:SSS").format(System.currentTimeMillis())
    println("$nowTime [$Thread.currentThread().name] $msg.joinToString(" ")")


/**
 * 进度通用回调  不使用flow封装的话 使用这个
 */
internal typealias ProgressBlock = (state: DownloadState) -> Unit

/**
 * 下载状态机
 */
sealed class DownloadState 

    /**
     * 未开始
     */
    object UnStart : DownloadState()

    /**
     * 下载中
     */
    class Progress(var totalNum: Long, var current: Long) : DownloadState()

    /**
     * 下载完成
     */
    class Complete(val file: File?) : DownloadState()

    /**
     * 下载失败
     */
    class Failure(val e: Throwable?) : DownloadState()

    /**
     * 下载失败
     */
    class FileExistsNoDownload(val file: File?) : DownloadState()


下载文件,带进度,一般封装

fun downloadFile(url: String, destFileDirName: String, progressBlock: ProgressBlock) 
    //下载状态  默认未开始
    var state: DownloadState = DownloadState.UnStart
    progressBlock(state)

    // TODO: 2021/12/27 file 创建与判断可以封装
    /**
     * file 创建判断  可以封装
     */
    val file = File(destFileDirName)
    val parentFile = file.parentFile
    if (!parentFile.exists()) 
        parentFile.mkdirs()
    
    if (file.exists()) 
        //文件存在 不需要下载
        state = DownloadState.FileExistsNoDownload(file)
        progressBlock(state)
        return
     else 
        file.createNewFile()
    

    //下载
    val okHttpClient = OkHttpClient()
    val request = Request.Builder().url(url).build()
    okHttpClient.newCall(request).enqueue(object : Callback 
        override fun onFailure(call: Call, e: IOException) 
            state = DownloadState.Failure(e)
            progressBlock(state)
        

        override fun onResponse(call: Call, response: Response) 
            response.use  res ->
                //完整长度
                var totalLength = 0L
                //写入字节
                val bytes = ByteArray(2048)
                val fileOutputStream = FileOutputStream(file)
                res.body?.also  responseBody ->
                    totalLength = responseBody.contentLength()
                ?.byteStream()?.let  inputStream ->
                    try 
                        var currentProgress = 0L
                        var len = 0
						state = DownloadState.Progress(totalLength, currentProgress)
                        do 
                            if (len != 0) 
                                currentProgress += len
                                fileOutputStream.write(bytes)
                            
                            //状态改变
                            (state as DownloadState.Progress).current = currentProgress
                            progressBlock(state)
                            len = inputStream.read(bytes, 0, bytes.size)
                         while (len != -1)
                        //状态改变完成
                        state = DownloadState.Complete(file)
                        progressBlock(state)
                     catch (e: Exception) 
                        state = DownloadState.Failure(e)
                        progressBlock(state)
                     finally 
                        inputStream.close()
                        fileOutputStream.close()
                    
                
            
        

    )


使用

    downloadFile(
        "https://dldir1.qq.com/weixin/Windows/WeChatSetup.exe",
        "download/WeChatSetup.exe"
    )  state: DownloadState ->
        when (val s = state) 
            is DownloadState.Complete -> log("下载完成 文件路径为 $s.file?.absoluteFile")
            is DownloadState.Failure -> log("下载失败  $s.e?.message")
            is DownloadState.FileExistsNoDownload -> log("已经存在  $s.file?.absoluteFile")
            is DownloadState.Progress -> log("下载中  $(s.current.toFloat() / s.totalNum) * 100%")
            DownloadState.UnStart -> log("下载未开始")
        
    

使用flow封装

对于上述封装使用起来没有问题,但是如果在android上面要把进度显示出来的话,就需要手动切换到UI线程了。不太方便。既然都用kotlin了,那么为什么不解除协程Flow封装呢?

所以,下面基于Flow的封装就来了。直接切换到Main线程,美滋滋。

知识储备:
Kotlin:Flow 全面详细指南,附带源码解析。
Flow : callbackFlow使用心得,避免踩坑!

/**
 * 使用Flow改造文件下载
 * callbackFlow  可以保证线程的安全  底层是channel
 */
fun downloadFileUseFlow(url: String, destFileDirName: String) = callbackFlow<DownloadState> 
    var state: DownloadState = DownloadState.UnStart
    send(state)

    //获取文件对象
    val file = File(destFileDirName).also  file ->
        val parentFile = file.parentFile
        if (!parentFile.exists()) 
            parentFile.mkdirs()
        
        if (file.exists()) 
            state = DownloadState.FileExistsNoDownload(file)
            send(state)
            //流关闭,返回
            close()
            return@callbackFlow
         else 
            file.createNewFile()
        
    
    //下载
    val okHttpClient = OkHttpClient().newBuilder()
        .dispatcher(dispatcher)
        .writeTimeout(30, TimeUnit.MINUTES)
        .readTimeout(30, TimeUnit.MINUTES)
        .build()
    val request = Request.Builder()
        .url(url)
        .build()
    okHttpClient.newCall(request).enqueue(object : Callback 
        override fun onFailure(call: Call, e: IOException) 
            //更新状态
            state = DownloadState.Failure(e)
            this@callbackFlow.trySendBlocking(state)
            close()
        

        override fun onResponse(call: Call, response: Response) 
            //下载
            val body = response.body
            if (response.isSuccessful && body != null) 
                //完整长度
                val totalNum: Long = body.contentLength()
                //当前下载的长度
                var currentProgress: Long = 0L
                var len = 0

                response.use 
                    //等效于   FileOutputStream(file)  输出流
                    val outputStream = file.outputStream()
                    //输入流
                    val byteStream = body.byteStream()
                    try 
                        val bates = ByteArray(2048)

                        //设置状态对象拉出来,避免循环一直创建对象
                        state = DownloadState.Progress(totalNum, currentProgress)
                        //循环读写
                        do 
                            if (len != 0) 
                                currentProgress += len
                                outputStream.write(bates)
                            
                            //更新进度
                            (state as DownloadState.Progress).current = currentProgress
                            this@callbackFlow.trySendBlocking(state)
                            len = byteStream.read(bates, 0, bates.size)
                         while (len != -1)
                        //下载完成
                        state = DownloadState.Complete(file)
                        this@callbackFlow.trySendBlocking(state)
                     catch (e: Exception) 
                        state = DownloadState.Failure(e)
                        this@callbackFlow.trySendBlocking(state)
                     finally 
                        outputStream.close()
                        byteStream.close()
                        //关闭callbackFlow
                        this@callbackFlow.close()
                    
                

             else 
                //更新状态且关闭
                state = DownloadState.Failure(Exception(response.message))
                this@callbackFlow.trySendBlocking(state)
                close()
            
        

    )
    //使用channelFlow 必须使用awaitClose 挂起flow等待channel结束
    awaitClose 
        log("callbackFlow关闭 .")
    

    .buffer(Channel.CONFLATED) //设置 立即使用最新值 buffer里面会调用到fuse函数,继而调用到create函数重新创建channelFlow
    .flowOn(Dispatchers.Default) //直接设置callbackFlow执行在异步线程
    .catch  e ->
        //异常捕获重新发射
        emit(DownloadState.Failure(e))
    

使用

    //这里使用runBlocking只是为了跑程序,一般和lifecycleScope等合作使用
    runBlocking(Dispatchers.Main) 
        downloadFileUseFlow(
            "https://dldir1.qq.com/weixin/Windows/WeChatSetup.exe",
            "download/WeChatSetup.exe"
        ).onEach  downloadState ->
            when (downloadState) 
                is DownloadState.Complete -> log("下载完成 文件路径为 $downloadState.file?.absoluteFile")
                is DownloadState.Failure -> log("下载失败  $downloadState.e?.message")
                is DownloadState.FileExistsNoDownload -> log("已经存在  $downloadState.file?.absoluteFile")
                is DownloadState.Progress -> log("下载中  $(downloadState.current.toFloat() / downloadState.totalNum) * 100%")
                DownloadState.UnStart -> log("下载未开始")
            
        .launchIn(this)
            .join()
    

以上就是博主提供的两种简单的封装方式了。

后面会陆续推出OkHttp高阶使用,以及OkHttp源码分析博客。觉得不错关注博主哈~😎
创作不易,如有帮助一键三连咯🙆‍♀️。欢迎技术探头噢!

以上是关于OkHttp初探2:如何使用OkHttp进行下载封装?带进度条?Kotlin+Flow版本。的主要内容,如果未能解决你的问题,请参考以下文章

从 OKHTTP 下载二进制文件

Android实战——okhttp3的使用和封装

使用OkHttp进行网络同步异步操作

OkHttp初探3:简单文件上传表单文件一起上传带进度条的文件上传MediaType介绍。Kotlin版本

okhttp-utils的封装之okhttp的使用

离线时使用 OKHttp 进行改造如何使用缓存数据