I want to call tasks asynchronously, so it can varied how long will each task take, but I want to wait for all of them to be finished in order to continue in next line under for loop. How can I achieve that? Because so far its not happening. Code below foreach should be called only if all of those tasks are finished. I'm looking for some "elegant" way to solve this, I dont wanna use some while loop and freeze entire thread.
Code:
val failedTasks = mutableListOf<ApiTask<T>>()
App.log("apiCallChained: startCall")
apiBlocks.forEachIndexed { i, apiBlock->
App.log("apiCallChained: callingTask: $i")
launch(Dispatchers.Main){
val (r, err) = withContext(Dispatchers.IO){
try {
apiBlock.apiBlock(this) to null
} catch (e: ApiCallError) {
null to e
}
}
when {
r != null -> {
App.log("apiCallChained: callingTask: $i - success")
apiBlock.onSuccess(r)
}
err != null -> {
App.log("apiCallChained: callingTask: $i - error")
failedTasks.add(apiBlock)
}
}
}
}
App.log("apiCallChained: endCall: ${failedTasks.size}")
if (failedTasks.isEmpty()) onSuccess.invoke() else onError.invoke(failedTasks)
Logs:
apiCallChained: startCall
apiCallChained: callingTask: 0
apiCallChained: callingTask: 1
apiCallChained: callingTask: 2
apiCallChained: callingTask: 3
apiCallChained: callingTask: 4
apiCallChained: endCall: 0
apiCallChained: callingTask: 2 - success
apiCallChained: callingTask: 3 - success
apiCallChained: callingTask: 4 - success
apiCallChained: callingTask: 0 - success
apiCallChained: callingTask: 1 - success
The only way two ways to do this without freezing the thread is with a callback or by directly running all of it in a coroutine to begin with.
So either:
scope.launch {
//replacement for your code above, see below...
// code to call after work finished...
}
or, assuming your CoroutineScope defaults to the main thread and you want to react on the main thread, you can call a callback from inside your coroutine. It looks like you already are doing something like this with your onSuccess and onError callbacks.
// This could be a ()->Unit callback passed into the function that runs this code:
val onWorkFinished = {
// code to call after work finished...
}
scope.launch {
//replacement for your code above, see below...
onWorkFinished()
}
So, the code we are replacing your above code with is in a coroutine. The typical way to do a bunch of parallel work and wait for all of it is by using a coroutineScope block. The block suspends until all children coroutines finish. You can replace your above code with:
val failedTasks = mutableListOf<ApiTask<T>>()
App.log("apiCallChained: startCall")
coroutineScope {
apiBlocks.forEachIndexed { i, apiBlock->
launch(Dispatchers.Main){
val (r, err) = withContext(Dispatchers.IO){
try {
apiBlock.apiBlock(this) to null
} catch (e: ApiCallError) {
null to e
}
}
when {
r != null -> {
App.log("apiCallChained: callingTask: $i - success")
apiBlock.onSuccess(r)
}
err != null -> {
App.log("apiCallChained: callingTask: $i - error")
failedTasks.add(apiBlock)
}
}
}
}
}
App.log("apiCallChained: endCall: ${failedTasks.size}")
if (failedTasks.isEmpty()) onSuccess.invoke() else onError.invoke(failedTasks)
Incidentally, you can simplify it as follows:
val failedTasks = mutableListOf<ApiTask<T>>()
App.log("apiCallChained: startCall")
coroutineScope {
apiBlocks.forEachIndexed { i, apiBlock->
launch(Dispatchers.Main){
try {
val result = withContext(Dispatchers.IO) { apiBlock.apiBlock(this) }
App.log("apiCallChained: callingTask: $i - success")
apiBlock.onSuccess(result)
} catch (e: ApiCallError) {
App.log("apiCallChained: callingTask: $i - error")
failedTasks.add(apiBlock)
}
}
}
}
App.log("apiCallChained: endCall: ${failedTasks.size}")
if (failedTasks.isEmpty()) onSuccess.invoke() else onError.invoke(failedTasks)
Related
suspend fun copy(oldFile: File, newFile: File): Boolean{
return withContext(Dispatchers.IO) {
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
val fileReader = ByteArray(4096)
inputStream = oldFile.inputStream()
outputStream = FileOutputStream(newFile)
while (true) {
val read: Int = inputStream.read(fileReader)
if (read == -1) {
break
}
outputStream.write(fileReader, 0, read)
}
outputStream.flush()
true
} catch (e: IOException) {
Log.e(TAG, "${e.message}")
false
} finally {
inputStream?.close()
outputStream?.close()
}
}
}
In the above code, if I cancel the job that is running the function, does the copying gets cancelled or do I have to manually check for state of the job inside while loop using ensureActive()?
Hello I have something like:
Observable.fromIterable(0..4)
.map { if (it == 3) throw Exception() else it }
.subscribe { println(it) }
I wish it to return all values except one (it == 3 must be skipped due to error). How could it be done?
It can be done for example with wrapping in Maybe:
Observable.fromIterable(0..4)
.flatMapMaybe {
Maybe.fromCallable { if (it == 3) throw Exception() else it }
.onErrorComplete()
}
.subscribe { println(it) }
Will the following code leak resources when the Kotlin coroutine is canceled?
General: The code is nested inside a ViewModel!
The method retrievePDFDocument will get triggered in the onStart Event of a Fragment.
fun retrievePDFDocument() {
job = viewModelScope.launch {
withContext(Dispatchers.IO) {
downloadFile(assetPath.value!!)
}
}
}
And here the suspend function:
private fun downloadFile(strPdfUrl: String): File? {
var inputStream: InputStream? = null
val lenghtOfFile: Int //lenghtOfFile is used for calculating download progress
//this is where the file will be seen after the download
var fileOut: FileOutputStream? = null
var localPDFFile: File? = null
if(strPdfUrl.isBlank())
return localPDFFile
try {
val pdfUrl = URL(strPdfUrl)
val urlConnection = pdfUrl.openConnection() as HttpURLConnection
if (urlConnection.responseCode == 200) {
//file input is from the url
inputStream = BufferedInputStream(urlConnection.inputStream)
lenghtOfFile = urlConnection.contentLength
localPDFFile = File(localPdfDirectory, pdfFileName.value!!)
fileOut = FileOutputStream(localPDFFile)
//here’s the download code
val buffer = ByteArray(1024)
var total: Long = 0
while (true) {
// If coroutine is cancelled
// a CancellationException will be thrown here
// Do not more work then necesarry
coroutineContext.ensureActive()
val length = inputStream.read(buffer)
if (length <= 0) break
total += length.toLong()
_currProgress.postValue( (total * 100 / lenghtOfFile).toInt() )
fileOut.write(buffer, 0, length)
}
}
} catch (e: IOException) {
Log.e("PdfViewerViewModel - downloadFile Error: ${e.message}")
localPdfFile?.delete() // remove partially downloaded file
localPDFFile = null
} catch (e1: CancellationException) {
Log.e("PdfViewerViewModel - downloadFile CancellationException: ${e1.message}")
localPdfFile?.delete() // remove partially downloaded file
localPdfFile = null
finally {
try {
inputStream?.close()
fileOut?.apply {
flush()
close()
}
} catch (e1: IOException) { //do nothing here }
}
return localPDFFile
}
Kindly regards
Frank
#Update 10.04.2020
Implementation with coroutineContext.ensureActive() and catching the exception
Playground sample
Given this code, the exception thrown in getRecords() is not caught in testFlattenMerge() - should it not be catchable though? Also, in getPeople(), the exception can be caught which causes flattenMerge() to work as expected, but it prints out "Caught in people" before any numbers are printed, and not after 64, as I expected. Is this the correct behavior? I can't quite fit my mental model of flattenMerge() around this.
import kotlin.reflect.KProperty
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
fun getRecords(id: Int) = flow {
repeat(5) { emit("A record for $id") }
if (id == 6) throw RuntimeException("Anything but #6!")
repeat(5) { emit("A record for $id") }
}
fun getPeople() = flow {
repeat(10) { emit(getRecords(it)) }
// repeat(10) { emit(getRecords(it).catch{ println("Caught in getPeople()")}) } // This works, but it prints /before/ any cnt lines...?
}
suspend fun testFlattenMerge() {
println ("Merge with flattenMerge()")
var cnt = 0
val flowOfFlows = getPeople()
flowOfFlows.catch{ println("Caught before flattenMerge")}
.flattenMerge()
.catch{ println("Caught after flattenMerge")}
.collect {
println("${cnt++}") // Without catching inside getPeople() this stops at 64
}
}
suspend fun testManualMerge() {
println("Merge manually")
var cnt = 0
repeat(10) {
getRecords(it).catch{ println("Caught in manual merge") }
.collect {
println("${cnt++}") // This goes up to 94, as expected
}
}
}
fun main() = runBlocking {
testFlattenMerge()
testManualMerge()
}
Why is the code below not logging to the console a TimeoutCancellationException?
#Test fun noExceptionLogged(){
GlobalScope.launch{
withTimeout(4000) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
Thread.sleep(20_000)
}
It seems like it works like this due to GlobalScope nature. It cannot be canceled so it swallows CancellationException.
GlobalScope.launch { // won't print anything
throw CancellationException()
}
GlobalScope.launch { // will print stacktrace
throw RuntimeException()
}
runBlocking { // will print stackrace
throw RuntimeException()
}
GlobalScope.launch { // will print "Hello!"
try {
throw CancellationException()
} catch (e: Exception) {
println("Hello!")
}
}