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

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

文章目录

    • 通用模块封装
    • 下载文件,带进度,一般封装
    • 使用flow封装

通用模块封装

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

/**
 * 日志打印
 */
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,0,len)
                            }
                            //状态改变
                            (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,0,len)
                            }
                            //更新进度
                            (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,my,kotlin,android,java,okhttp)