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)
}
}
}
}
}
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).
Related
I have a Ktor class where I need to do action on unauthorized exception(when token is expired), for this action I need to have synchronized action, otherwise it is not working correctly, the problem is that #Synchronized is not synchronize the action and is not waiting for action to finish for next one.
fun ktorFunction(){
HttpResponseValidator {
handleResponseException { exception ->
kermit.e { "In ${exception.message}" }
val clientException =
exception as? ClientRequestException ?: return#handleResponseException
val exceptionResponse = clientException.response
when (exceptionResponse.status) {
HttpStatusCode.Unauthorized -> {
test(){
kermit.v { "Error message" }
}
}
}
}
}
#Synchronized
fun test(messageTest: () -> Unit) {
CoroutineScope(Dispatchers.Default).launch {
delay(3000)
messageTest()
}
}
The idea is I want test function to not be called from other thread until it is finished, whatever the action is in it.
launch is a function that starts a coroutine asynchronously and immediately returns, so this is behaving as it should. If you want to synchronize coroutines, you should use Mutex.
I'll leave it alone in the example before, but IMO, it's a code smell to create a CoroutineScope if you're not going to manage its lifecycle.
private val testMutex = Mutex()
fun test(messageTest: () -> Unit) {
CoroutineScope(Dispatchers.Default).launch {
testMutex.withLock {
delay(3000)
messageTest()
}
}
}
Is there a way to limit the number of collector in a function that returns a Flow using flow builder?
I have this public method in a ViewModel
fun fetchAssets(limit: String) {
viewModelScope.launch {
withContext(Dispatchers.IO){
getAssetsUseCase(AppConfigs.ASSET_PARAMS, limit).onEach {
when (it) {
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
This method is called on ViewModel's init block, but can also be called manually on UI.
This flow emits value every 10 seconds.
Repository
override fun fetchAssets(
query: String,
limit: String
) = flow {
while (true) {
try {
interceptor.baseUrl = AppConfigs.ASSET_BASE_URL
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(service.getAssetItems(query, limit))
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
delay(10_000)
}
}
Unfortunately every time fetch() was invoke from UI, I noticed that it creates another collectors thus can ended up having tons of collector which is really bad and incorrect.
The idea is having a flow that emits value every 10 seconds but can also be invoke manually via UI for immediate data update without having multiple collectors.
You seem to misunderstand what does it mean to collect the flow or you misuse the collect operation. By collecting the flow we mean we observe it for changes. But you try to use collect() to introduce changes to the flow, which can't really work. It just starts another flow in the background.
You should collect the flow only once, so keep it inside init or wherever it is appropriate for your case. Then you need to update the logic of the flow to make it possible to trigger reloading on demand. There are many ways to do it and the solution will differ depending whether you need to reset the timer on manual update or not. For example, we can use the channel to notify the flow about the need to reload:
val reloadChannel = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets(
query: String,
limit: String
) = flow {
while (true) {
try {
...
}
withTimeoutOrNull(10.seconds) { reloadChannel.receive() } // replace `delay()` with this
}
}
fun reload() {
reloadChannel.trySend(Unit)
}
Whenever you need to trigger the manual reload, do not start another flow or invoke another collect() operation, but instead just invoke reload(). Then the flow that is already being collected, will start reloading and will emit state changes.
This solution resets the timer on manual reload, which I believe is better for the user experience.
I ended up moving the timer on ViewModel as I can request on demand fetch while also not having multiple collectors that runs at the same time.
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
Please correct me if this one is a code smell.
I am reading this article to fully understand the dos and donts of using Flow while comparing it to my implementation, but I can't grasp clearly how to tell if you are wasting resource when using Flow or flow builder. When is the time a flow is being release/freed in memory and when is the time that you are wasting resource like accidentally creating multiple instances of flow and not releasing them?
I have a UseCase class that invokes a repository function that returns Flow. In my ViewModel this is how it looks like.
class AssetViewModel constructor(private val getAssetsUseCase: GetAssetsUseCase) : BaseViewModel() {
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
}
The idea here is we are fetching data from remote every 10 seconds while also allowing on demand fetch of data via UI.
Just a typical useless UseCase class
class GetAssetsUseCase #Inject constructor(
private val repository: AssetsRepository // Passing interface not implementation for fake test
) {
operator fun invoke(baseUrl: String, query: String, limit: String): Flow<RequestStatus<AssetDomain>> {
return repository.fetchAssets(baseUrl, query, limit)
}
}
The concrete implementation of repository
class AssetsRepositoryImpl constructor(
private val service: CryptoService,
private val mapper: AssetDtoMapper
) : AssetsRepository {
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
) = flow {
try {
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
}
}
After reading this article which says that using stateIn or sharedIn will improve the performance when using a flow, it seems that I am creating new instances of the same flow on-demand. But there is a limitation as the stated approach only works for variable and not function that returns Flow.
stateIn and shareIn can save resources if there are multiple observers, by avoiding redundant fetching. And in your case, you could set it up to automatically pause the automatic re-fetching when there are no observers. If, on the UI side you use repeatOnLifecycle, then it will automatically drop your observers when the view is off screen and then you will avoid wasted fetches the user will never see.
I think it’s not often described this way, but often the multiple observers are just observers coming from the same Activity or Fragment class after screen rotations or rapidly switching between fragments. If you use WhileSubscribed with a timeout to account for this, you can avoid having to restart your flow if it’s needed again quickly.
Currently you emit to from an external coroutine instead of using shareIn, so there’s no opportunity to pause execution.
I haven't tried to create something that supports both automatic and manual refetching. Here's a possible strategy, but I haven't tested it.
private val refreshRequest = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets() {
refreshRequest.trySend(Unit)
}
val assetState = flow {
while(true) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).map {
when(it){
is RequestStatus.Loading -> AssetState.FetchLoading
is RequestStatus.Success -> AssetState.FetchSuccess(it.data.assetDataDomain)
is RequestStatus.Failed -> AssetState.FetchFailed(it.message)
}
}.emitAll()
withTimeoutOrNull(100L) {
// drop any immediate or pending manual request
refreshRequest.receive()
}
// Wait until a fetch is manually requested or ten seconds pass:
withTimeoutOrNull(10000L - 100L) {
refreshRequest.receive()
}
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(4000L), replay = 1)
To this I would recommend not using flow as the return type of the usecase function and the api call must not be wrapped inside a flow builder.
Why:
The api call actually is happening once and then again after an interval it is triggered by the view model itself, returning flow from the api caller function will be a bad usage of powerful tool that is actually meant to be called once and then it must be self-reliant, it should emit or pump in the data till the moment it has a subscriber/collector.
One usecase you can consider when using flow as return type from the room db query call, it is called only once and then the room emits data into it till the time it has subscriber.
.....
fun fetchAssets() {
viewModelScope.launch {
// loading true
val result=getusecase(.....)
when(result){..process result and emit on state..}
// loading false
}
}
.....
suspend operator fun invoke(....):RequestStatus<AssetDomain>{
repository.fetchAssets(baseUrl, query, limit)
}
.....
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
):RequestStatus {
try {
//RequestStatus.Loading()//this can be managed in viewmodel itself
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
RequestStatus.Success(domainModel)
} catch (e: HttpException) {
RequestStatus.Failed(e)
} catch (e: IOException) {
RequestStatus.Failed(e)
}
}
In my project I write View and ViewModel natively and share Repository, Db, networking.
When user navigates from one screen to another, I want to cancel all network requests or other heavy background operations that are currently running in the first screen.
Example function in Repository class:
#Throws(Throwable::class)
suspend fun fetchData(): List<String>
In Android's ViewModel I can use viewModelScope to automatically cancel all active coroutines. But how to cancel those tasks in iOS app?
Lets suppose that the object session is a URLSession instance, you can cancel it by:
session.invalidateAndCancel()
I didn't find any first party information about this or any good solution, so I came up with my own. Shortly, it will require turning repository suspend functions to regular functions with return type of custom interface that has cancel() member function. Function will take action lambda as parameter. On implementation side, coroutine will be launched and reference for Job will be kept so later when it is required to stop background work interface cancel() function will cancel job.
In addition, because it is very hard to read type of error (in case it happens) from NSError, I wrapped return data with custom class which will hold error message and type. Earlier I asked related question but got no good answer for my case where ViewModel is written natively in each platform.
If you find any problems with this approach or have any ideas please share.
Custom return data wrapper:
class Result<T>(
val status: Status,
val value: T? = null,
val error: KError? = null
)
enum class Status {
SUCCESS, FAIL
}
data class KError(
val type: ErrorType,
val message: String? = null,
)
enum class ErrorType {
UNAUTHORIZED, CANCELED, OTHER
}
Custom interface
interface Cancelable {
fun cancel()
}
Repository interface:
//Convert this code inside of Repository interface:
#Throws(Throwable::class)
suspend fun fetchData(): List<String>
//To this:
fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable
Repository implementation:
override fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable = runInsideOfCancelableCoroutine {
val result = executeAndHandleExceptions {
val data = networkExample()
// do mapping, db operations, etc.
data
}
action.invoke(result)
}
// example of doing heavy background work
private suspend fun networkExample(): List<String> {
// delay, thread sleep
return listOf("data 1", "data 2", "data 3")
}
// generic function for reuse
private fun runInsideOfCancelableCoroutine(task: suspend () -> Unit): Cancelable {
val job = Job()
CoroutineScope(Dispatchers.Main + job).launch {
ensureActive()
task.invoke()
}
return object : Cancelable {
override fun cancel() {
job.cancel()
}
}
}
// generic function for reuse
private suspend fun <T> executeAndHandleExceptions(action: suspend () -> T?): Result<T> {
return try {
val data = action.invoke()
Result(status = Status.SUCCESS, value = data, error = null)
} catch (t: Throwable) {
Result(status = Status.FAIL, value = null, error = ErrorHandler.getError(t))
}
}
ErrorHandler:
object ErrorHandler {
fun getError(t: Throwable): KError {
when (t) {
is ClientRequestException -> {
try {
when (t.response.status.value) {
401 -> return KError(ErrorType.UNAUTHORIZED)
}
} catch (t: Throwable) {
}
}
is CancellationException -> {
return KError(ErrorType.CANCELED)
}
}
return KError(ErrorType.OTHER, t.stackTraceToString())
}
}
You probably have 3 options:
If you're using a some sort of reactive set up iOS side (e.g. MVVM) you could just choose to ignore cancellation. Cancellation will only save a minimal amount of work.
Wrap your iOS calls to shared code in an iOS reactive framework (e.g. combine) and handle cancellation using the iOS framework. The shared work would still be done, but the view won't be updated as your iOS framework is handling cancellation when leaving the screen.
Use Flow with this closable helper
I have a retrofit service
interface Service {
#PUT("path")
suspend fun dostuff(#Body body: String)
}
It is used in android view model.
class VM : ViewModel(private val service: Service){
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
val state = MutableLiveData<String()
init {
uiScope.launch {
service.doStuff()
state.value = "lol"
}
}
override fun onCleared(){
viewModelJob.cancel()
}
}
I would like to write a test for the cancelling of the view model. This will be done mocking service and delaying so that the co routine does not complete. Whilst blocking, we invoke onCleared to cancel the co routine. This should prevent state getting set...
#Test
fun `on cleared - cancels request`() = runBlocking {
//given
`when`(service.doStuff()).thenAnswer { launch { delay(1000) } }
val vm = ViewModel(service)
// when
vm.cleared()
//then
assertThat(vm.state, nullValue())
}
However it seems that vm.state always gets set??? What is the best way to test when clearing a scope that a co routine gets cancelled?
The problem here is in thenAnswer { launch { delay(1000) } }, which effectively makes your doStuff method look like that:
suspend fun doStuff() {
launch { delay(1000) }
}
As you can see, this function does not actually suspend, it launches a coroutine and returns immediately. What would actually work here is thenAnswer { delay(1000) }, which does not work, because there is no suspend version of thenAnswer in Mockito (as far as I know at least).
I would recommend to switch to Mokk mocking library, which supports kotlin natively. Then you can write coEvery { doStuff() } coAnswers { delay(1000) } and it will make your test pass (after fixing all the syntax errors ofc).