Retrofit 协程 下载

demo: https://pan.baidu.com/s/1-u2Z7x9G19VPweK9VvwKtA   提取码:8zv2

              https://download.csdn.net/download/jingzz1/13008973

整个下载过程需要用到Retrofit+协程+LiveData+Lifecycle

封装完后,只需要一步就能下载:


        GlobalScope.launch {
            download("下载地址",DowloadBuild(context))
                .collect {
                    when (it) {
                        is DowloadStatus.DowloadErron -> {
                            //下载错误
                        }
                        is DowloadStatus.DowloadSuccess -> {
                            //下载完成
                        }
                        is DowloadStatus.DowloadProcess -> {
                            //下载中
                            //下载进度:it.process
                        }
                    }
                }
        }

下载 

//Service的写法
interface UrlService {
    //通用下载
    @Streaming
    @GET
    suspend fun downloadFile(@Url url:String): Response<ResponseBody>
}

封装过程:

首先是下载状态类

三个状态:下载中,下载完成,下载错误

sealed class DowloadStatus {
    class DowloadProcess(val currentLength: Long, val length: Long, val process: Float) :DowloadStatus()
    class DowloadErron(val t: Throwable) : DowloadStatus()
    class DowloadSuccess(val uri: Uri) : DowloadStatus()
}

下载设置类

abstract class IDowloadBuild {
    open fun getFileName(): String? = null
    open fun getUri(contentType: String): Uri? = null
    open fun getDowloadFile(): File? = null
    abstract fun getContext(): Context //贪方便的话,返回Application就行
}

class DowloadBuild(val cxt: Context):IDowloadBuild(){
    override fun getContext(): Context = cxt
}

使用协程异步流的方式下载文件

fun download(url: String, build: IDowloadBuild) = flow{
    //UrlService.downloadFile(),这部分不用我教了吧
    val response = RetrofitUtils.create().downloadFile(url)

    response.body()?.let { body ->
        val length = body.contentLength()
        val contentType = body.contentType().toString()
        val ios = body.byteStream()
        val info = try {
            dowloadBuildToOutputStream(build, contentType)
        } catch(e:Exception){
            emit(DowloadStatus.DowloadErron(e))
            DowloadInfo(null)
            return@flow
        }
        val ops = info.ops
        if (ops == null) {
            emit(DowloadStatus.DowloadErron(RuntimeException("下载出错")))
            return@flow
        }
        //下载的长度
        var currentLength: Int = 0
        //写入文件
        val bufferSize = 1024 * 8
        val buffer = ByteArray(bufferSize)
        val bufferedInputStream = BufferedInputStream(ios, bufferSize)
        var readLength: Int = 0
        while (bufferedInputStream.read(buffer, 0, bufferSize)
                .also { readLength = it } != -1
        ) {
            ops.write(buffer, 0, readLength)
            currentLength += readLength
            emit(
                DowloadStatus.DowloadProcess(
                    currentLength.toLong(),
                    length,
                    currentLength.toFloat() / length.toFloat()
                )
            )
        }
        bufferedInputStream.close()
        ops.close()
        ios.close()
        if (info.uri != null)
            emit(DowloadStatus.DowloadSuccess(info.uri))
        else emit(DowloadStatus.DowloadSuccess(Uri.fromFile(info.file)))

    } ?: kotlin.run {
        emit(DowloadStatus.DowloadErron(RuntimeException("下载出错")))
    }
}.flowOn(Dispatchers.IO)

private fun dowloadBuildToOutputStream(build: IDowloadBuild, contentType: String): DowloadInfo {
    val context = build.getContext()
    val uri = build.getUri(contentType)
    if (build.getDowloadFile() != null) {
        val file = build.getDowloadFile()!!
        return DowloadInfo(FileOutputStream(file), file)
    } else if (uri != null) {
        return DowloadInfo(context.contentResolver.openOutputStream(uri), uri = uri)
    } else {
        val name = build.getFileName()
        val fileName = if(!name.isNullOrBlank()) name else "${System.currentTimeMillis()}.${
            MimeTypeMap.getSingleton()
                .getExtensionFromMimeType(contentType)
        }"
        val file = File("${context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)}",fileName)
        return DowloadInfo(FileOutputStream(file), file)
    }
}

private class DowloadInfo(val ops: OutputStream?, val file: File? = null, val uri: Uri? = null)

这样,整个过程就封装好了,然后是调用下载

完整的下载过程

        val dowloadUrl = "下载地址"
        download(dowloadUrl, object : IDowloadBuild() {
            //返回context 这是必需的,当然这里也可以直接返回Application
            override fun getContext(): Context = MyApplication.context

            //可以实现以下三个方法中的任意一个,当然,也可以不实现,不实现的话文件名和类型会从网址获取,文件会保存到私有目录
            override fun getDowloadFile(): File? {
                //返回存储下载的文件File("存储地址+文件名"),可以在这里设置保存文件地址
                return File(getContext().cacheDir, "app.apk")
            }

            override fun getFileName(): String? {
                //返回保存文件的文件名
                return "app.apk"
            }
            
            //android10之后如果下载的文件需要传递给外部app,建议直接下载成uri
            override fun getUri(contentType: String): Uri? {
                //下载到共享目录,这里需要考虑android10以上
                val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    val values = ContentValues().apply {
                        put(MediaStore.MediaColumns.DISPLAY_NAME, "xxxx.后缀") //文件名
                        put(MediaStore.MediaColumns.MIME_TYPE, contentType) //文件类型
                        put(
                            MediaStore.MediaColumns.RELATIVE_PATH,
                            Environment.DIRECTORY_DOWNLOADS
                        ) 
                    }
                    getContext().contentResolver.insert(
                        MediaStore.Downloads.EXTERNAL_CONTENT_URI,
                        values
                    )
                } else
                    Uri.fromFile(File(getContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "文件名.后缀"))
                return uri
            }

        })
           .collect  {
                when (it) {
                    is DowloadStatus.DowloadErron -> {
                        //下载错误
                    }
                    is DowloadStatus.DowloadSuccess -> {
                        //下载成功
                        //下载的uri为:
                        val uri = it.uri
                        //uri转file可以参考:https://blog.csdn.net/jingzz1/article/details/106188462
                    }
                    is DowloadStatus.DowloadProcess -> {
                        //已下载长度 :it.currentLength
                        //文件总长度:it.length
                        //下载进度: it.process
                    }
                }
            }

流式操作

download()是一个异步流,即表示他可以使用流操作

 download(dowloadUrl,DowloadBuild(this@DownloadActivity))
               //.flowOn(Dispatchers.IO)//线程切换
               .catch {  }//异常处理
               .onStart { LogUtils.e("下载开始") }//流开始
               .onCompletion { LogUtils.e("下载结束") }//流结束
                ……

消费流

同样的道理,末端调用 collect 流才能开始
download(dowloadUrl,DowloadBuild(this@DownloadActivity))
               //.flowOn(Dispatchers.IO)//线程切换
               .collect {
                   when(it){
                       is DowloadStatus.DowloadProcess ->{
                           LogUtils.e(it.process)
                       }
                   }
               }

冷流

同理,流是冷的,可以在多个地方开启流

        lifecycleScope.launchWhenCreated {
            val dow = download(dowloadUrl, DowloadBuild(this@DownloadActivity))

            dow.collect {
                if (it is DowloadStatus.DowloadProcess)
                    LogUtils.e(it.process)
            }
            dow.collect {
                if (it is DowloadStatus.DowloadErron)
                    LogUtils.e(it.t)
                else if (it is DowloadStatus.DowloadProcess)
                    LogUtils.e(it.process)
            }
        }

分享流

转成热流后,同一个流可以在不同协程中被感知

分享流需要在协程1.4之后才支持:添加依赖

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
        val dow = download(dowloadUrl, DowloadBuild(this@DownloadActivity))
            .shareIn(lifecycleScope, SharingStarted.Eagerly)

        //两个协程接收的是同一个流值
        lifecycleScope.launch{
            dow.collect {
                if (it is DowloadStatus.DowloadProcess)
                    LogUtils.e(it.process)
            }
        }

        lifecycleScope.launch{
            dow.collect {
                if (it is DowloadStatus.DowloadProcess)
                    LogUtils.e(it.process)
            }
        }

猜你喜欢

转载自blog.csdn.net/jingzz1/article/details/108324102