【问题标题】:Handling file download with gRPC on Android在 Android 上使用 gRPC 处理文件下载
【发布时间】:2020-02-15 12:09:19
【问题描述】:

我目前有一个正在发送视频文件块的 gRPC 服务器。我用 Kotlin 编写的 android 应用程序使用协程进行 UI 更新(在 Dispatchers.MAIN 上)和处理单向块流(在 Dispatchers.IO 上)。像下面这样:

GlobalScope.launch(Dispatchers.Main) {
   viewModel.downloadUpdated().accept(DOWNLOAD_STATE.DOWNLOADING) // MAKE PROGRESS BAR VISIBLE

      GlobalScope.launch(Dispatchers.IO) {
         stub.downloadVideo(request).forEach {
                file.appendBytes(
                    it.data.toByteArray()
                )
            }
      }.join()
      viewModel.downloadUpdated().accept(DOWNLOAD_STATE.FINISHED) // MAKE PROGRESS BAR DISAPPEAR
   } catch (exception: Exception) {
      viewModel.downloadUpdated().accept(DOWNLOAD_STATE.ERROR) // MAKE PROGRESS BAR DISAPPEAR
      screenNavigator.showError(exception) // SHOW DIALOG
   }
}

这很好用,但我想知道是否没有一种“更干净”的方式来处理下载。我已经知道 DownloadManager,但我觉得它只接受 HTTP 查询,所以我不能使用我的 gRPC 存根(我可能错了,如果是,请告诉我)。我还检查了 WorkManager,这是同样的问题,我不知道这是否是处理该案例的正确方法。

所以,这里有两个问题:

  • 有没有办法以干净的方式处理 gRPC 查询,这意味着我现在可以在它开始、完成、失败时进行处理,并且我可以正确取消?
  • 如果没有,有没有更好的方法来使用协程?

编辑

对于那些感兴趣的人,我相信我想出了一个虚拟算法,用于在更新进度条的同时进行下载(对改进开放):

suspend fun downloadVideo(callback: suspend (currentBytesRead: Int) -> Unit) {
   println("download")

   stub.downloadVideo(request).forEach {
      val data = it.data.toByteArray()

      file.appendBytes(data)
      callback(x) // Where x is the percentage of download
   }
        
    println("downloaded")
}

class Fragment : CoroutineScope { //NOTE: The scope is the current Fragment
    private val job = Job()
    
    override val coroutineContext: CoroutineContext
        get() = job
    
    fun onCancel() {
        if (job.isActive) {
            job.cancel()
        }
    }
    
    private suspend fun updateLoadingBar(currentBytesRead: Int) {
        println(currentBytesRead)
    }
    
    fun onDownload() {
     
        launch(Dispatchers.IO) {
            downloadVideo { currentBytes ->
                withContext(Dispatchers.Main) {
                        
                    updateLoadingBar(currentBytes)
                    
                    if (job.isCancelled)
                        println("cancelled !")
                }
            }
        }
    }
}

更多信息,请查看:Introduction to coroutines

编辑 2

正如 cmets 中所建议的,我们实际上可以使用 Flows 来处理这个问题,它会给出如下结果:

suspend fun foo(): Flow<Int> = flow { 
   println("download")
   stub.downloadVideo(request).forEach {
      val data = it.data.toByteArray()

      file.appendBytes(data)
      emit(x) // Where x is the percentage of download
   }
   println("downloaded")
}

class Fragment : CoroutineScope {
    private val job = Job()
    
    override val coroutineContext: CoroutineContext
        get() = job
    
    fun onCancel() {
        if (job.isActive) {
            job.cancel()
        }
    }
    
    private suspend fun updateLoadingBar(currentBytesRead: Int) {
        println(currentBytesRead)
    }
    
    fun onDownload() {
     
        launch(Dispatchers.IO) {
            withContext(Dispatchers.Main) {
                foo()
                    .onCompletion { cause -> println("Flow completed with $cause") }
                    .catch { e -> println("Caught $e") }
                    .collect { current -> 
                        if (job.isCancelled)
                            return@collect

                        updateLoadingBar(current)
                }
            }
        }
    }
}

【问题讨论】:

  • wire 从 3.0 版本开始支持 gRPC。它可能对您的用例有所帮助。它也支持协程。 Here 是他们谈论它的视频。
  • @Emmanuel 他们现在支持第 3 版协议缓冲区吗?
  • 我认为没有
  • 是的,我已经在 pas 中使用了wire,我改变了,因为它只支持版本 2。但它肯定有更好的 kotlin 支持,所以我可能会考虑搬回去。
  • grpc服务器下载功能的小技巧呢?

标签: android kotlin grpc kotlin-coroutines


【解决方案1】:

gRPC 可以是很多东西,所以在这方面你的问题还不清楚。最重要的是,它可以完全异步和基于回调,这意味着它可以变成一个Flow,你可以在主线程上收集它。但是,文件写入是阻塞的。

您的代码似乎会在后台启动下载后立即发送FINISHED 信号。您应该将launch(IO) 替换为withContext(IO)

【讨论】:

  • 是的,gRPC 可以做很多事情,在这里应用程序接收文件块流。然后我在启动(Dispatchers.IO)之后忘记了join(),我更新了它。请注意,我添加了“单向块流”和连接功能。
  • 您应该将launch + join 替换为withContext。如果在 gRPC 级别上,您有一个单向的块流,它仍然没有说明您使用什么 API 来使用它。我的建议是使用Flow。流都是自包含的,这使得异常处理变得简单:您只需将流收集到try-catch 中。您还可以通过取消正在收集它的协程来取消流。
  • 很难展示如何将现有流转换为流的伪代码,因为我不知道它背后的内容。如果你有,你会写chunkFlow.collect { file.writeChunk(it) }
  • 您的新代码具有显式回调处理和显式上下文切换。就“它可以工作”而言,它是可以的,但我觉得如果你让 Flow 处理其中一些问题,你可以减少手工工作。
  • 您可以进一步改进它,将flowOn(Dispatchers.IO) 添加为suspend fun foo 的最后一行(顺便说一句,不再需要suspend)并从调用方删除两个withContext 包装器.
猜你喜欢
  • 1970-01-01
  • 2019-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多