在Android上使用gRPC处理文件下载

在Android上使用gRPC处理文件下载,android,kotlin,grpc,kotlin-coroutines,Android,Kotlin,Grpc,Kotlin Coroutines,我目前有一个gRPC服务器,它正在发送视频文件的块。我用Kotlin编写的android应用程序使用协同路由进行UI更新(在Dispatchers.MAIN上)和处理块的单向流(在Dispatchers.IO上)。例如: GlobalScope.launch(Dispatchers.Main) { viewModel.downloadUpdated().accept(DOWNLOAD_STATE.DOWNLOADING) // MAKE PROGRESS BAR VISIBLE

我目前有一个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查询,这意味着我现在可以在它开始、结束、失败时进行处理,并且可以正确地取消查询
  • 如果没有,有没有更好的方法来使用协程呢
编辑 对于那些感兴趣的人,我相信我提出了一个虚拟算法,可以在更新进度条时下载(可以改进):

有关更多信息,请查看:

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

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)
                }
            }
        }
    }
}
suspend fun foo():Flow=Flow{
println(“下载”)
stub.downloadVideo(请求).forEach{
val data=it.data.toByteArray()
file.appendBytes(数据)
emit(x)//其中x是下载的百分比
}
println(“下载”)
}
类片段:CoroutineScope{
private val job=job()
覆盖val coroutineContext:coroutineContext
get()=作业
乐趣{
if(job.isActive){
作业。取消()
}
}
private suspend fun updateLoadingBar(currentByteRead:Int){
println(当前字节读取)
}
下载的乐趣(){
发射(Dispatchers.IO){
withContext(Dispatchers.Main){
foo()
.onCompletion{cause->println(“流已用$cause完成”)}
.catch{e->println(“catch$e”)}
.收集{当前->
如果(作业已取消)
return@collect
updateLoadingBar(当前)
}
}
}
}
}

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


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

gRPC可能有很多问题,因此在这方面您的问题并不清楚。最重要的是,它可以是完全异步和基于回调的,这意味着它可以变成一个
,您可以在主线程上收集它。然而,文件写入是阻塞的


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

是的,gRPC可以做很多事情,在这里,应用程序会接收文件块流。然后我忘记了join(),就在发布之后(Dispatchers.IO),我更新了它。请注意,我添加了“块的单向流”和连接函数。您应该将
launch
+
join
替换为
withContext
。如果在gRPC级别上,您有一个单向块流,它仍然没有说明您使用什么API。我的建议是使用
Flow
。流都是关于自包含的,这使得异常处理变得简单:您只需在
try catch
中收集流。您还可以通过取消收集流的协同程序来取消流。很难显示如何将现有流转换为流的伪代码,因为我不知道它背后是什么。如果您有它,您将编写
chunkFlow.collect{file.writeChunk(it)}
。您的新代码具有显式回调处理和显式上下文切换。就“它可以工作”而言,这是可以的,但我觉得如果你让Flow处理其中一些问题,你可以通过更少的手工工作来解决。你可以进一步改进它,添加
flowOn(Dispatchers.IO)
作为
suspend fun foo
中的最后一行(顺便说一句,它不再需要
suspend
)然后从调用端删除
withContext
wrapper。是的,gRPC可以做很多事情,在这里,应用程序接收到一个文件块流。然后我忘记了join(),就在发布之后(Dispatchers.IO),我更新了它。请注意,我添加了“块的单向流”和连接函数。您应该将
launch
+
join
替换为
withContext
。如果在gRPC级别上,您有一个单向块流,它仍然没有说明您使用什么API。我的建议是使用
Flow
。流都是关于自包含的,这使得异常处理变得简单:您只需在
try catch
中收集流。您还可以通过取消收集流的协同程序来取消流。很难显示如何将现有流转换为流的伪代码,因为我不知道它背后是什么。如果您有它,您将编写
chunkFlow.collect{file.writeChunk(it)}
。您的新代码具有显式回调处理和显式上下文切换。就“它可以工作”而言,这是可以的,但我觉得如果你让Flow处理其中一些问题,你可以少做一些手工工作。你可以进一步改进它,添加
flowOn(Dispatchers.IO)
作为
suspend fun foo
中的最后一行(顺便说一句,没有必要)
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)
                }
            }
        }
    }
}