问题
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