Android中使用Retrofit下载文件并显示下载进度:LiveData与Flow的综合应用与比较

1,012 阅读6分钟

前言

移动应用开发中,处理文件下载是一项常见的任务,无论是下载图像、音频、视频还是其他类型的文件。为了提供更好的用户体验,我们不仅需要能够下载大文件,还需要在下载过程中显示进度,以便用户可以清晰地了解下载的状态。同时,随着Android开发中的新技术的涌现,我们还需要考虑如何以最佳方式管理和传递数据流。

在本文中,我们将探讨如何在Android应用中使用Retrofit这一强大的网络库来实现文件下载,并且将展示如何监控下载进度,以使用户能够实时追踪文件的下载状态。但这并不是全部,我们还将比较两种不同的数据流管理工具:LiveData和Flow。LiveData是Android架构组件的一部分,而Flow是Kotlin的库,用于处理异步数据流。通过比较它们的特性和适用场景,您将能够更好地了解如何在不同情况下选择使用其中之一。

接下来,我们将从文件下载和进度监控开始,然后深入研究LiveData和Flow的使用方式以及它们各自的优势和限制。

Retrofit

Retrofit是一款强大的Android网络请求库,它使与RESTful API的通信变得非常简单。它基于OkHttp库构建,提供了一个高级的、类型安全的API来定义和执行HTTP请求。使用Retrofit,您可以轻松地执行GET、POST等各种HTTP请求,并处理数据。 Retrofit 2.6.0 版本引入了对挂起函数(suspend functions)的支持,使得在 Kotlin 协程中使用 Retrofit 更加直观和便捷。以下是有关 Retrofit 支持挂起函数的简介:

Retrofit 和挂起函数

  1. 协程友好:Retrofit 的挂起函数支持是为了更好地与 Kotlin 协程集成。协程是一种异步编程模型,能够轻松处理异步操作,如网络请求。通过支持挂起函数,Retrofit 允许您在协程中执行网络请求,从而简化了异步代码的编写。

  2. 更清晰的异步代码:传统的 Retrofit 调用是基于回调的,这会导致回调嵌套和难以理解的异步代码。支持挂起函数后,您可以使用 suspend 修饰符来定义网络请求方法,使代码更加清晰、可读和结构化。这意味着您可以以同步的方式编写异步代码,而不需要回调。

  3. 异常处理:Retrofit 的挂起函数能够利用协程的异常处理机制。这意味着您可以使用 try-catch 块捕获和处理网络请求过程中可能出现的异常,从而更容易处理错误情况。

  4. 线程切换:挂起函数可以在协程上下文中运行,这使得您可以轻松地在后台线程执行网络请求并在主线程中处理结果,而不需要手动进行线程切换。

LiveData

LiveData是Android架构组件的一部分,主要用于处理UI相关的数据。它的设计目标是让数据与UI的生命周期绑定,以确保数据的更新和UI的同步。因此,LiveData是一种"热"数据流(hot stream),它在有观察者(Observer)时才会传递数据变化。

  • LiveData是Android架构组件的一部分,旨在用于处理UI相关的数据。

  • LiveData是基于观察者模式的,它可以感知Activity或Fragment的生命周期,确保数据更新只在活动生命周期内通知观察者。

  • LiveData通常与ViewModel一起使用,以在ViewModel中存储和管理数据,并在UI组件中观察它们的变化。

  • LiveData不支持冷流(cold stream),因此不适合用于非UI相关的后台任务。

Flow

Kotlin Flow 是 Kotlin 语言的一个库,用于处理异步数据流。它是一种强大的工具,允许您以响应式编程的方式处理和操作数据流。以下是关于 Kotlin Flow 的简单介绍:

  • Kotlin Flow 旨在处理异步操作的结果,如网络请求、数据库查询、传感器数据等。它允许您将这些数据流建模为数据流序列,并以响应式方式处理它们。
  • Kotlin Flow 与 Kotlin 协程紧密集成,这意味着它可以轻松地与协程一起使用。这使得编写异步代码更加简单和可读,因为您可以使用 suspend 函数和其他协程构造来管理异步操作。
  • Kotlin Flow 支持冷流和热流。可以用于任何需要处理异步数据的场景,不仅仅是UI。
  • Kotlin Flow 提供了丰富的操作符和转换器,使您能够对数据流进行各种操作,例如映射、过滤、组合和转换。这些操作符使得处理数据流变得非常灵活。
  • Kotlin Flow 提供了背压支持,允许您处理产生数据速度大于消费速度的情况。您可以使用 bufferconflate 等操作符来控制背压策略。

实战

基于Retrofit 下载文件并且展示进度(这里的文件使用图片)

动图展示:

1.gif

2.gif

Retrofit

构建RetrofitManager实例

object RetrofitManager {

   private const val BASEURL = EndPoints.BASE_URL


   val imageService: ImageService by lazy { retrofit.create(ImageService::class.java) }
   /**
    * 懒加载
    */
   private val retrofit: Retrofit by lazy {
       buildRetrofit(BASEURL, buildHttpClient())
   }

   /**
    * 构建自己的OKHttp
    */
   private fun buildHttpClient(): OkHttpClient.Builder {
       val logging = HttpLoggingInterceptor()
       logging.level = HttpLoggingInterceptor.Level.BODY
       return OkHttpClient.Builder()
           .addInterceptor(logging)
           .connectTimeout(20, TimeUnit.SECONDS)
           .readTimeout(20, TimeUnit.SECONDS)


   }

   /**
    * 构建 Retrofit
    */
   private fun buildRetrofit(baseUrl: String, build: OkHttpClient.Builder): Retrofit {

       val client = build.build()
       return Retrofit.Builder()
           .baseUrl(baseUrl)
           .addConverterFactory(GsonConverterFactory.create())
           .client(client).build()
   }
}

下载图片接口

ImageSevice

interface ImageService {

     @Streaming
     @GET(EndPoints.IMAGE)
     suspend fun  getImage(): Response<ResponseBody>

     @Streaming
     @GET(EndPoints.IMAGE2)
     suspend fun getImage2(): Response<ResponseBody>



}

LiveData代码演示

class ImageViewModel :ViewModel() {

    /**
     * LiveData
     */
    private val _downloadLiveData = MutableLiveData<DownloadState>()
    private val downloadLiveData :LiveData<DownloadState>
        get() = _downloadLiveData

    init {


    }

    /**
     * 异常信息
     */
    private val exception = CoroutineExceptionHandler { _, throwable ->
        _downloadLiveData.value = DownloadState.Failed(throwable.message)
    }

    fun getImage( image:String):LiveData<DownloadState>{
        viewModelScope.launch(exception) {
            val targetFile = File(image)
            _downloadLiveData.postValue(DownloadState.Loading)
            var response = RetrofitManager.imageService.getImage()
            if(response.isSuccessful) {
                response.body()?.let { saveFile(it, targetFile) }
            } else{
                _downloadLiveData.postValue(DownloadState.Failed(response.errorBody().toString()))
            }

        }
        return downloadLiveData
    }





    /**
     * 封装saveFile
     */
    private suspend fun saveFile(response:ResponseBody, targetFile:File) = withContext(Dispatchers.IO){
        val inputStream = response.byteStream()
        val outputStream = FileOutputStream(targetFile)
        val contentLength: Long = response.contentLength()
        var len = 0
        var sum = 0
        val bytes = ByteArray(1024 * 12)
        while (inputStream.read(bytes).also { len = it } != -1) {
            sum += len
            outputStream.write(bytes, 0, len)
            outputStream.flush()
            val sumLen = sum
            // 模拟耗时
            delay(50)
            _downloadLiveData.postValue(DownloadState.Downloading((sumLen * 1.0f / contentLength * 100).toInt()))
        }
        targetFile.setExecutable(true, false)
        targetFile.setReadable(true, false)
        targetFile.setWritable(true, false)
        _downloadLiveData.postValue(DownloadState.DownloadFinish(targetFile.absolutePath))
    }
}

View 监听:

val targetFile = File(requireActivity().cacheDir, "image.png")
  imageViewModel.getImage(targetFile.absolutePath)
      .observe(viewLifecycleOwner) { downloadState ->
          when (downloadState) {

              is DownloadState.Loading ->{
                  binding.buttonFirst.text = "正在下载中"
              }
              is DownloadState.Downloading -> {
                  binding.buttonFirst.text = "${downloadState.progress}%"
              }

              is DownloadState.DownloadFinish -> {
                  val bitmap = BitmapFactory.decodeFile(downloadState.filePath)
                  binding.imageView.setImageBitmap(bitmap)
                  binding.buttonFirst.text = "下载完成"
              }

              is DownloadState.Failed -> {
                  binding.buttonFirst.text = downloadState.error
              }

          }

      }

}

Flow代码演示

class ImageViewModelByFlow : ViewModel() {



    private val _dataFlow = MutableStateFlow<DownloadState>(DownloadState.Downloading(0))
    private val dataFlow: StateFlow<DownloadState> = _dataFlow.asStateFlow()

    /**
     * 异常信息
     */
    private val exception = CoroutineExceptionHandler { _, throwable ->

        viewModelScope.launch {
            _dataFlow.emit(DownloadState.Failed(throwable.message))
        }

    }

    suspend fun getImage2(image: String): Flow<DownloadState> {
        viewModelScope.launch(exception){
            val targetFile = File(image)
            _dataFlow.emit(DownloadState.Loading)
            var responseBody = RetrofitManager.imageService.getImage2()
            if(responseBody.isSuccessful){
                responseBody.body()?.saveFile(targetFile)
            }else{
                _dataFlow.emit((DownloadState.Failed(responseBody.errorBody().toString())))

            }

        }
        return dataFlow


    }

    /**
     * save file
     */
    private suspend fun ResponseBody.saveFile(targetFile: File) = withContext(Dispatchers.IO){
        val inputStream = byteStream()
        val outputStream = FileOutputStream(targetFile)
        val contentLength: Long = contentLength()
        var len = 0
        var sum = 0
        val bytes = ByteArray(1024 * 12)
        while (inputStream.read(bytes).also { len = it } != -1) {
            sum += len
            outputStream.write(bytes, 0, len)
            outputStream.flush()
            val sumLen = sum
            // 模拟耗时
            delay(50)
            _dataFlow.emit(DownloadState.Downloading((sumLen * 1.0f / contentLength * 100).toInt()))
        }
        targetFile.setExecutable(true, false)
        targetFile.setReadable(true, false)
        targetFile.setWritable(true, false)
        _dataFlow.emit(DownloadState.DownloadFinish(targetFile.absolutePath))
    }


}

View 中监听:

val targetFile = File(requireActivity().cacheDir, "image.png")
imageViewModel.getImage2(targetFile.absolutePath).collect{
    downloadState ->
    when (downloadState) {

        is DownloadState.Loading ->{
            binding.buttonFirst.text = "正在下载中"
        }

        is DownloadState.Downloading -> {
            binding.buttonFirst.text = "${downloadState.progress}%"
        }

        is DownloadState.DownloadFinish -> {
            val bitmap = BitmapFactory.decodeFile(downloadState.filePath)
            binding.imageView.setImageBitmap(bitmap)
            binding.buttonFirst.text = "下载完成"
        }

        is DownloadState.Failed -> {
            binding.buttonFirst.text = downloadState.error
        }

    }

总结

  • 综上所述,LiveData 适用于处理与 UI 相关的数据流,它与 Android 架构组件协同工作,确保数据更新与生命周期一致。而 Kotlin Flow 更通用,适用于处理各种异步操作,提供更多的灵活性和异常处理能力
  • LiveData和Flow不是互斥的关系,它们可以在Android应用中共同使用,但Flow是基于协程的并且同时支持冷流和热流,功能上更强大一些。

源码

github.com/ThirdPrince…