kotlin 使用 协程 + okhttp3 + flow 实现文件下载

该博客详细介绍了Android应用中使用协程进行网络请求和文件下载的实现过程,包括配置依赖、权限设置、下载逻辑实现、错误处理等。涉及到的关键技术有Kotlin协程、Retrofit、OkHttp以及文件管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

配置

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'

    
    //协程的
    api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
    api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
    //协程的(在viewmodel 使用的)
    api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'

    //网络的
    api 'com.squareup.retrofit2:retrofit:2.9.0'
    api 'com.squareup.retrofit2:converter-moshi:2.9.0'
    
}

配置2

  <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
    <!--用于写入缓存数据到扩展存储卡-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
  
<!--    android 11 的要加这句话-->
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

    <application
        android:requestLegacyExternalStorage="true"
        >
....

调用

download(mContext!!)

实现:1

   fun download(context: Context) {
        val time = System.currentTimeMillis()
        LogUtil.d("time1:${time}")
        val url = "https://xxxx"
 
        val file2 = getTargetParentFile(context)
        viewModelScope.launch {
            DownloadGO.download(
                url = url,
                targetParent = file2.path
            ).collect { downloadStatus ->
                when (downloadStatus) {
                    is DownloadStatus.onProgress -> {
                        LogUtil.d("currentThread:${Thread.currentThread().name}")
                        LogUtil.d("progress:${downloadStatus.progress}")
                    }
                    
                    is DownloadStatus.onSuccess -> {

                        LogUtil.d("耗时:${System.currentTimeMillis() - time}")
                        LogUtil.d("currentThread:${Thread.currentThread().name}")
                        LogUtil.d("onSuccess file:${downloadStatus.file?.path}")
                    }

                    is DownloadStatus.onFail -> {
                        LogUtil.d("下载失败 error:${downloadStatus?.error}")
                    }
                    else -> {
                        LogUtil.d("下载失败")
                    }
                }
            }

            LogUtil.d("处理完成")
        }
    }

fun getTargetParentFile(context: Context): File {
        val file0 = Environment.getExternalStorageDirectory().absolutePath
        val file1 = File("${file0}/MyDownload")
        if (!file1.exists() || !file1.isDirectory) {
            file1.mkdirs()
        }
        return file1
    }

下载的逻辑

 
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import okhttp3.*
import retrofit2.HttpException
import java.io.*
import java.net.UnknownHostException

/**
 * 作者:Lambert
 * 时间:2022-09-08
 */
object DownloadGO {
    /**
     * @param url 文件下载连接
     * @param targetParent 需要保存到指定文件夹目录(tips: 是文件夹)
     */
    fun download(url: String, targetParent: String): Flow<DownloadStatus> {

        var bufferedInputStream: BufferedInputStream? = null
        var outputStream: FileOutputStream? = null
        var inputStream: InputStream? = null

        return flow {

            //下载前先检查 是否已经存在本地了
            val localFile = checkDownloadFile(url, targetParent)
            if (localFile != null) {
                //这里表示本地已经存在了已经下载好的文件
                LogUtil.d("文件已经存在")
                emit(DownloadStatus.onSuccess(localFile))

            } else {
                //执行下载

                val request = Request.Builder().url(url).get().build()
                val response = OkHttpClient.Builder().build().newCall(request).execute()

                val code = response.code


                LogUtil.d("download: code :${code}")

                //网络有响应成功,且body 不为空
                LogUtil.d("download: response.isSuccessful :${response.isSuccessful}")

                if (response.isSuccessful  ) {

                    saveFile(url, targetParent, response).collect {
                        emit(it)
                    }

                } else {
                    //请求失败的情况
                    if (code == 404) {
                        emit(DownloadStatus.onUrlUnkownFail())
                    }
                    emit(DownloadStatus.onFail(null))
                }
            }

        }.catch { error ->
            //捕获到异常了
            LogUtil.d("catch:error:${error}")

            if (error is UnknownHostException) {
                //无网络
                emit(DownloadStatus.onNetFail())
            }
            emit(DownloadStatus.onFail(null))

        }.onCompletion {
            bufferedInputStream?.close()
            outputStream?.close()
            inputStream?.close()
        }.flowOn(Dispatchers.IO)
    }

    fun saveFile(
        url: String,
        targetParent: String,
        response: Response,
    ): Flow<DownloadStatus> {

        var bufferedInputStream: BufferedInputStream? = null
        var outputStream: FileOutputStream? = null
        var inputStream: InputStream? = null

        LogUtil.d("1-2")

        return flow {


            val body = response.body
            val contentLength:Long = body!!.contentLength()
            inputStream = body!!.byteStream()

            LogUtil.d("contentLength:${contentLength}")

            //使用临时文件保存
            val tmpFile = getFileTmpName(url)
            val file = File(targetParent, tmpFile)

            outputStream = FileOutputStream(file)

            val bufferSize = 1024 * 8
            val buffer = ByteArray(bufferSize)


            bufferedInputStream = BufferedInputStream(inputStream, bufferSize)

            var readLength: Int
            var currentLength = 0L

            var oldProgress = 0L
            while (bufferedInputStream!!.read(buffer, 0, bufferSize)
                    .also { readLength = it } != -1
            ) {

                outputStream!!.write(buffer, 0, readLength)
                currentLength += readLength

                val currentProgress = currentLength * 100 / contentLength
//                LogUtil.d("currentProgress:${currentProgress} \n currentLength:${currentLength}")
                if (currentProgress - oldProgress >= 1) {
                    oldProgress = currentProgress
                    emit(DownloadStatus.onProgress(currentProgress))
                }

            }


            emit(DownloadStatus.onProgress(100L))

            //修改文件名字
            val newFile = updataFile(file, targetParent, url)
            //返回下载成功
            emit(DownloadStatus.onSuccess(newFile))

        }.onCompletion {
            bufferedInputStream?.close()
            outputStream?.close()
            inputStream?.close()
        }.catch { error ->
            LogUtil.d("error:${error}")

        }

    }


    fun updataFile(oldFile: File, targetPath: String, downloadUrl: String): File {

        val fileType = downloadUrl.substring(downloadUrl.lastIndexOf("."))
        LogUtil.d("fileType:${fileType}")
        var fileName = Md5.md5String(downloadUrl);
        fileName = "${fileName}${fileType}"

        //修改文件名为正式的
        val newPathFile = File(targetPath, fileName)
        oldFile.renameTo(newPathFile)

        return if (newPathFile.exists()) {
            newPathFile
        } else {
            oldFile
        }

    }

    //检查要下载的文件是否已经存在本地了
    private fun checkDownloadFile(fileUrl: String, outputFile: String): File? {

        val fileType = fileUrl.substring(fileUrl.lastIndexOf("."))

        var fileName = Md5.md5String(fileUrl);

        fileName = "${fileName}${fileType}"
        val localFile = File(outputFile, fileName)

        return if (localFile.exists()) {
            localFile
        } else {
            null
        }
    }

    //临时文件
    private fun getFileTmpName(fileUrl: String): String {
        return "${Md5.md5String(fileUrl)}.tmp"
    }
}

sealed class DownloadStatus {

    /**
     * 下载进度
     */
    data class onProgress(val progress: Long?) : DownloadStatus()

    /**
     * 下载成功
     */
    data class onSuccess(val file: File?) : DownloadStatus()

    /**
     * 网络错误(网络断开或未连接)
     */
    class onNetFail() : DownloadStatus()

    /**
     * 下载地址错误
     */
    class onUrlUnkownFail() : DownloadStatus()
    /**
     * 下载错误
     */
    data class onFail(val error: Throwable?) : DownloadStatus()

}
/**
 * 作者 : Lambert
 * 时间:2022-09-08
 */
class Md5 {

    companion object{
        fun md5String(text: String): String {
            try {
                //获取md5加密对象
                val instance: MessageDigest = MessageDigest.getInstance("MD5")
                //对字符串加密,返回字节数组
                val digest:ByteArray = instance.digest(text.toByteArray())
                var sb = StringBuffer()
                for (b in digest) {
                    //获取低八位有效值
                    var i :Int = b.toInt() and 0xff
                    //将整数转化为16进制
                    var hexString = Integer.toHexString(i)
                    if (hexString.length < 2) {
                        //如果是一位的话,补0
                        hexString = "0$hexString"
                    }
                    sb.append(hexString)
                }
                return sb.toString()

            } catch (e: NoSuchAlgorithmException) {
                e.printStackTrace()
            }

            return ""
        }
    }
}

LogUtil

class LogUtil {

    companion object{

        private const val TAG = "日志"

        fun d(content:Any){
            d(TAG,content)
        }

        fun d(tag:String,content:Any){
            if (BuildConfig.DEBUG) {
                Log.d(tag, content as String)
            }
        }

        @JvmStatic
        fun e(content:Any){
            e(TAG,content)
        }

        fun e(tag:String,content:Any){
            if (BuildConfig.DEBUG) {
                Log.e(tag, content as String)
            }
        }


    }
}

首次下载

再次下载同一个文件的时候

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值