文件下载是flow最经典的应用。
应用很简单,就是点击下载,更新进度条。
核心类
package com.dongnaoedu.flowpractice.download
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.IOException
//导入我们自己写的copyTo,覆盖系统的
import com.dongnaoedu.flowpractice.utils.copyTo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOn
/**
*单例模式(object)就可以,
*/
object DownloadManager {
//返回一个Flow,里面保存下载的进度,异常,完成,错误等信息,所以Flow需要指定泛型类型
fun download(url: String, file: File): Flow<DownloadStatus> {
//
return flow {
val request = Request.Builder().url(url).get().build()
val response = OkHttpClient.Builder().build().newCall(request).execute()
if (response.isSuccessful) {//响应成功
//body是个可空类型,我们使用!!强制执行,如果body为空,就报空指针异常。
response.body()!!.let { body ->
val total = body.contentLength()
//文件读写(while循环),使用use函数
file.outputStream().use { output ->
val input = body.byteStream()
var emittedProgress = 0L
//copyTo是我们自己写的一个扩展函数
input.copyTo(output) { bytesCopied ->
val progress = bytesCopied * 100 / total
//只发5的整数倍进度
if (progress - emittedProgress > 5) {
delay(100)//下载太快了,看不清楚,加一个延时
emit(DownloadStatus.Progress(progress.toInt()))
emittedProgress = progress
}
}
}
}
//下载完成后发送出去
emit(DownloadStatus.Done(file))
} else {
//response没有响应成功,就抛个异常
throw IOException(response.toString())
}
//flow的catch操作符,把上游的异常可以catch到,
}.catch {
file.delete()
//把catch到的异常it发送出去
emit(DownloadStatus.Error(it))
//指定调度器,IO操作放到IO调度器上
}.flowOn(Dispatchers.IO)
}
}
bean类
package com.dongnaoedu.flowpractice.download
import java.io.File
/**
*密封类,其成员都是其子类
*/
sealed class DownloadStatus {
//啥也没有,单例对象
object None : DownloadStatus()
//下载的进度
data class Progress(val value: Int) : DownloadStatus()
//下载的错误信息
data class Error(val throwable: Throwable) : DownloadStatus()
//下载完成,file:文件放到哪里了
data class Done(val file: File) : DownloadStatus()
}
扩展函数
package com.dongnaoedu.flowpractice.utils
import java.io.InputStream
import java.io.OutputStream
//边读边写
inline fun InputStream.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, progress: (Long)-> Unit): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
progress(bytesCopied)
}
return bytesCopied
}
在Fragment中使用
package com.dongnaoedu.flowpractice.fragment
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import com.dongnaoedu.flowpractice.R
import com.dongnaoedu.flowpractice.databinding.FragmentDownloadBinding
import com.dongnaoedu.flowpractice.databinding.FragmentHomeBinding
import com.dongnaoedu.flowpractice.download.DownloadManager
import com.dongnaoedu.flowpractice.download.DownloadStatus
import kotlinx.coroutines.flow.collect
import java.io.File
class DownloadFragment : Fragment() {
val URL = "http://192.168.1.4:8080/kotlinstudyserver/pic.JPG"
private val mBinding: FragmentDownloadBinding by lazy {
FragmentDownloadBinding.inflate(layoutInflater)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return mBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//启动协程,lifecycleScope与Activity/Fragment生命周期绑定,fragment移除的时候,协程就会取消
lifecycleScope.launchWhenCreated {
//context可能为空,使用apply操作符
context?.apply {
//本地文件
val file = File(getExternalFilesDir(null)?.path, "pic.JPG")
//collect会引发下载,collect是挂起函数,必须写在协程里面
DownloadManager.download(URL, file).collect { status ->
//根据不同的status做不同的处理
when (status) {
is DownloadStatus.Progress -> {
mBinding.apply {
progressBar.progress = status.value
tvProgress.text = "${status.value}%"
}
}
is DownloadStatus.Error -> {
Toast.makeText(context, "下载错误", Toast.LENGTH_SHORT).show()
}
is DownloadStatus.Done -> {
mBinding.apply {
progressBar.progress = 100
tvProgress.text = "100%"
}
Toast.makeText(context, "下载完成", Toast.LENGTH_SHORT).show()
}
else -> {
Log.d("ning", "下载失败.")
}
}
}
}
}
}
}
界面布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
tools:context=".fragment.DownloadFragment">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:max="100" />
<TextView
android:id="@+id/tv_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/progressBar"
android:layout_centerHorizontal="true"
android:textSize="20sp"
tools:text="10%" />
</RelativeLayout>