Kotlin协程+Retrofit下载文件并实现进度监听

Kotlin协程+Retrofit下载文件并实现进度监听

1.前言

网上很多Retrofit+RxJava下载文件的功能,这里我使用kotlin+协程的方式使用一下,并且结合workmanager使用下载

  DownloadWorker.startDownload(
                this@MainActivity,
                "https://dldir1.qq.com/wework/work_weixin/wxwork_android_3.0.31.13637_100001.apk",
                this@MainActivity.cacheDir.path,
                "wxwork_android_3.apk"
            )

2. 实现过程

1. 构建下载的ApiService

import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Streaming
import retrofit2.http.Url

interface ApiService {
 	@Streaming
 	@GET
 	suspend fun downloadFile(@Url fileUrl: String?): ResponseBody
 }

2. 声明相关监听方法

typealias download_error = suspend (Throwable) -> Unit
typealias download_process = suspend (downloadedSize: Long, length: Long, progress: Float) -> Unit
typealias download_success = suspend (uri: File) -> Unit

3. 构建Retrofit,并且获取ApiService

import retrofit2.Retrofit

object HttpKit {
    val retrofit = Retrofit.Builder()
        .baseUrl("http://www.xxx.com")
        .validateEagerly(true) //在开始的时候直接开始检测所有的方法
        .build()

    val apiService = retrofit.create(ApiService::class.java)
}				

4. 定义返回实体

/**
 * @author stj
 * @Date 2021/10/28-16:42
 * @Email 375105540@qq.com
 */

class HttpResult<out T> constructor(val value: Any?) {

    val isSuccess: Boolean get() = value !is Failure && value !is Progress

    val isFailure: Boolean get() = value is Failure

    val isLoading : Boolean get() = value is Progress

    fun exceptionOrNull(): Throwable? =
        when (value) {
            is Failure -> value.exception
            else -> null
        }
    /*
    .....代码省略
    */

    companion object {
        fun <T> success(value: T): HttpResult<T> =
            HttpResult(value)

        fun <T> failure(exception: Throwable): HttpResult<T> =
            HttpResult(createFailure(exception))

        fun <T> progress(currentLength: Long, length: Long, process: Float):HttpResult<T> =
            HttpResult(createLoading(currentLength, length, process))
    }

    data class Failure(val exception: Throwable)

    data class Progress(val currentLength: Long, val length: Long, val process: Float)
}


private fun createFailure(exception: Throwable): HttpResult.Failure =
    HttpResult.Failure(exception)


private fun createLoading(currentLength: Long, length: Long, process: Float) =
    HttpResult.Progress(currentLength, length, process)



inline fun <R, T> HttpResult<T>.fold(
    onSuccess: (value: T) -> R,
    onLoading:(loading: HttpResult.Progress) ->R,
    onFailure: (exception: Throwable?) -> R
): R {
    return when {
        isFailure -> {
            onFailure(exceptionOrNull())
        }
        isLoading -> {
            onLoading(value as HttpResult.Progress)
        }
        else -> {
            onSuccess(value as T)
        }
    }
}


5. 构建下载请求,已经处理

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.shetj.download.http.HttpKit.apiService
import java.io.BufferedInputStream
import java.io.File
import java.io.FileOutputStream

object KCHttpV2 {

    @JvmOverloads
    suspend fun download(
        url: String, outputFile: String,
        onError: download_error = {},
        onProcess: download_process = { _, _, _ -> },
        onSuccess: download_success = { }
    ) {
        flow {
            try {
                val body = apiService.downloadFile(url)
                val contentLength = body.contentLength()
                val inputStream = body.byteStream()
                val file = File(outputFile)
                val outputStream = FileOutputStream(file)
                var currentLength = 0
                val bufferSize = 1024 * 8
                val buffer = ByteArray(bufferSize)
                val bufferedInputStream = BufferedInputStream(inputStream, bufferSize)
                var readLength: Int
                while (bufferedInputStream.read(buffer, 0, bufferSize)
                        .also { readLength = it } != -1
                ) {
                    outputStream.write(buffer, 0, readLength)
                    currentLength += readLength
                    emit(
                        HttpResult.progress(
                            currentLength.toLong(),
                            contentLength,
                            currentLength.toFloat() / contentLength.toFloat()
                        )
                    )
                }
                bufferedInputStream.close()
                outputStream.close()
                inputStream.close()
                emit(HttpResult.success(file))
            } catch (e: Exception) {
                emit(HttpResult.failure<File>(e))
            }
        }.flowOn(Dispatchers.IO)
            .collect {
                it.fold(onFailure = { e ->
                    e?.let { it1 -> onError(it1) }
                }, onSuccess = { file ->
                    onSuccess(file)
                }, onLoading = { progress ->
                    onProcess(progress.currentLength, progress.length, progress.process)
                })
            }
    }
}

5. 请求示例

  lifecycleScope.launch{
  		apiService.download(downloadUrl, saveFile, 
			onProcess = { currentLength, length, process ->
             Log.i("download","${(process * 100).toInt()}%")
        	}, onSuccess = {
             Log.i("download","${it.absolutePath}%")
        	},onError = {
             it?.printStackTrace()
        	})
  }

3.配合workManager使用

1. 引入workManager

   def work_version = "2.7.0"
   implementation "androidx.work:work-runtime-ktx:$work_version"

2. 构建DownloadWorker


import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.Color
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.*
import me.shetj.download.demo.R
import me.shetj.download.http.KCHttpV2
import java.util.*


/**
 * 测试下载
 */
class DownloadWorker(context: Context, parameters: WorkerParameters) :
        CoroutineWorker(context, parameters) {

    override suspend fun doWork(): Result {
        val inputUrl = inputData.getString(KEY_INPUT_URL)
                ?: return Result.failure()
        val outputUrl = inputData.getString(KEY_OUT_PUT_URL)
                ?: return Result.failure()
        val filename = inputData.getString(KEY_OUTPUT_FILE_NAME)
                ?: return Result.failure()
        val progress = "Starting Download"
        setForeground(createForegroundInfo(progress))
        download(inputUrl, outputUrl, filename)
        return Result.success()
    }

    private suspend fun download(downloadUrl: String, outputFile: String, fileName: String) {
        KCHttpV2.download(downloadUrl, "$outputFile/$fileName", onProcess = { _, _, process ->
            setForeground(createForegroundInfo("${(process * 100).toInt()}%"))
            setProgress(Data.Builder().let {
                it.putInt("progress", (process * 100).toInt())
                it.build()
            })
        }, onSuccess = {
            setForeground(createForegroundInfo("download ok"))
        },onError = {
            it.printStackTrace()
        })
    }
    private fun createForegroundInfo(progress: String): ForegroundInfo {
        val intent = WorkManager.getInstance(applicationContext)
                .createCancelPendingIntent(id)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createChannel()
        }

        val notification = NotificationCompat.Builder(applicationContext, getChannelID())
                .setContentTitle(getTitle())
                .setTicker(getTitle())
                .setContentText(progress)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setOngoing(true) //防止滑动删除
                .addAction(R.mipmap.picture_icon_delete_photo , "取消", intent)
                .build()

        return ForegroundInfo("下载文件".hashCode(), notification)
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun createChannel() {
        if (NotificationManagerCompat.from(applicationContext).getNotificationChannel(
                        getChannelID()
                ) == null) {
            val name = "文件下载"
            val description = "文件下载"
            val importance = NotificationManager.IMPORTANCE_LOW
            val mChannel = NotificationChannel(getChannelID(), name, importance)
            mChannel.description = description
            mChannel.enableLights(true)
            mChannel.lightColor = Color.RED
            mChannel.enableVibration(true)
            return NotificationManagerCompat.from(applicationContext).createNotificationChannel(mChannel)
        }
    }

    private fun getTitle(): String {
        return "文件下载"
    }

    private fun getChannelID(): String {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            "文件下载"
        } else {
            ""
        }
    }

    companion object {
        const val KEY_INPUT_URL = "KEY_INPUT_URL"
        const val KEY_OUT_PUT_URL = "KEY_OUT_URL"
        const val KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME"

        fun startDownload(context: Context, downloadUrl: String, outputFile: String, fileName: String): UUID {
            val inputData: Data = Data.Builder().apply {
                putString(KEY_INPUT_URL, downloadUrl)
                putString(KEY_OUTPUT_FILE_NAME, fileName)
                putString(KEY_OUT_PUT_URL, outputFile)
            }.build()
            val request = OneTimeWorkRequestBuilder<DownloadWorker>().setInputData(inputData).build()
            WorkManager.getInstance(context).enqueue(request)
            return request.id
        }
    }
}

3. 使用示例

   DownloadWorker.startDownload(
                this@MainActivity,
                "https://dldir1.qq.com/wework/work_weixin/wxwork_android_3.0.31.13637_100001.apk",
                this@MainActivity.cacheDir.path,
                "wxwork_android_3.apk"
            )

demo下载

因为之前没有添加demo,现在加上
downloadDemo

  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
好的,我来回答你的问题。 首先,Kotlin 是一种基于 JVM 的静态类型编程语言,它的语法简洁易懂,支持函数式编程和面向对象编程。 协程Kotlin 中的一种轻量级线程,可以实现异步编程和并发执行。Retrofit 是一款网络请求库,它可以帮助我们轻松地实现网络请求和数据解析。MVVM 是一种软件架构模式,可以将应用程序分为三个部分:模型、视图和视图模型。 下面是一个基于 Kotlin + 协程 + Retrofit + MVVM 的网络请求的优雅实现: 1. 定义 API 接口 首先定义 API 接口,使用 Retrofit 注解来描述请求方法和参数。 ```kotlin interface ApiService { @GET("api/news") suspend fun getNews(@Query("category") category: String): NewsResponse } ``` 2. 创建数据模型 根据 API 接口的返回数据,我们可以创建一个数据模型。 ```kotlin data class News(val title: String, val content: String) data class NewsResponse(val code: Int, val message: String, val newsList: List<News>) ``` 3. 创建 ViewModel ViewModel 是连接数据模型和视图的中间层,它处理数据逻辑并提供可观察的数据。 ```kotlin class NewsViewModel : ViewModel() { private val _newsList = MutableLiveData<List<News>>() val newsList: LiveData<List<News>> = _newsList fun loadNews(category: String) { viewModelScope.launch { val response = retrofit.create(ApiService::class.java).getNews(category) if (response.code == 200) { _newsList.value = response.newsList } } } } ``` 4. 创建视图 视图负责展示数据,并与用户交互。 ```kotlin class NewsActivity : AppCompatActivity() { private val viewModel by viewModels<NewsViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_news) viewModel.newsList.observe(this, Observer { newsList -> // 更新视图 }) viewModel.loadNews("tech") } } ``` 通过使用 Kotlin + 协程 + Retrofit + MVVM,我们可以实现优雅地网络请求,代码简洁易懂,逻辑清晰。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TieJun~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值