demo: https://pan.baidu.com/s/1-u2Z7x9G19VPweK9VvwKtA 提取码:8zv2
整个下载过程需要用到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.filesDir}",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)
}
}