Handling file download with gRPC on Android

你说的曾经没有我的故事 提交于 2020-06-26 13:53:13

问题


I currently have a gRPC server which is sending chunks of a video file. My android application written in Kotlin uses coroutines for UI updates (on Dispatchers.MAIN) and for handling a unidirectional stream of chunks (on Dispatchers.IO). Like the following:

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
   }
}

This works pretty well but I wonder if there is not a 'cleaner' way to handle downloads. I already know about DownloadManager but I feel like it only accepts HTTP queries and so I can't use my gRPC stub (I might be wrong, please tell me if so). I also checked WorkManager, and here is the same problem I do not know if this is the proper way of handling that case.

So, there are two questions here:

  • Is there a way to handle gRPC queries in a clean way, meaning that I can now when it starts, finishes, fails and that I can cancel properly?
  • If not, is there a better way to use coroutines for that ?

EDIT

For those interested, I believe I came up with a dummy algorithm for downloading while updating the progress bar (open to improvments):

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 !")
                }
            }
        }
    }
}

For more info, please check: Introduction to coroutines

EDIT 2

As proposed in comments we could actually use Flows to handle this and it would give something like:

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)
                }
            }
        }
    }
}

回答1:


gRPC can be many things so in that respect your question is unclear. Most importantly, it can be fully async and callback-based, which would mean it can be turned into a Flow that you can collect on the main thread. File writing, however, is blocking.

Your code seems to send the FINISHED signal right away, as soon as it has launched the download in the background. You should probably replace launch(IO) with withContext(IO).



来源:https://stackoverflow.com/questions/60238635/handling-file-download-with-grpc-on-android

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!