How to find out if a coroutine job is delayed - kotlin

Is there an elegant way to find out if a job is currenty delayed?
I need to restart a job, but only if it is being delayed, like this
fun restart() {
if (job?.isDelayed()) job?.cancel()
else return
job = launch {
repeat(10) {
//do some heavy work
delay(5000)
}
}
}

The answer can be found from the Job documentation, there is no Delay state for Job. There are several states of New, Active, Completing, Cancelling, Cancelled, Completed in the job, and the state flow provided by the job also flows in these states.
It seems that it is impossible to judge whether it is in the delay state from the Job alone. We can set the delay flag in the class separately to change the state before/after the delay function call. The following is my test code, I hope it can help you.
object Test {
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var job: Job? = null
private var delayFlag = false
fun startJob() {
if (job?.isActive == true) return
job = scope. launch {
repeat(10) {
delayFlag = true
delay(5000)
delayFlag = false
}
}
}
fun restart() {
if (job?.isActive == true && delayFlag) job?.cancel() else return
println("restart job")
job = scope. launch {
repeat(10) {
//do some heavy work
delayFlag = true
delay(5000)
delayFlag = false
}
}
}
}
fun main() = runBlocking {
Test. startJob()
repeat(5) {
Test. restart()
delay(20000L)
}
}

Related

Launch a new coroutine task when the existing one gets cancelled in kotlin

I would like to recreate a new coroutine for the task if the existing one gets cancelled because of an exception or other reasons. Currently, the only way I can detect the cancellation is via the invokeOnCompletion handler and I need to trigger new job creation for the cancelled task somehow, also invokeOnCompletion is not a suspend function so I have to do some trick there.
It's my current workaround
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
lateinit var backGroundTask: Job
suspend fun main(): Unit = supervisorScope {
val restartChannel = Channel<Boolean>()
val restartBackGroundTaskListener = startListeningForRestartEvent(restartChannel)
backGroundTask = someBackGroundTask()
backGroundTask.invokeOnCompletion {
launch { restartChannel.send(true) }
}
val restartBackGroundTask = restartBackGroundTaskIntermittently()
delay(1000)
restartBackGroundTask.join()
restartBackGroundTaskListener.join()
}
fun CoroutineScope.startListeningForRestartEvent(restartEvents: Channel<Boolean>): Job =
launch {
for (event in restartEvents) {
when (event) {
true -> {
println("newJob created ..")
backGroundTask = someBackGroundTask()
backGroundTask.invokeOnCompletion {
launch { restartEvents.send(true) }
}
}
false -> {}
}
}
}
fun CoroutineScope.someBackGroundTask(): Job =
launch {
repeat(1000) {
delay(400)
println(it)
}
}
// Just to simulate the cancellation due to some reason
fun CoroutineScope.restartBackGroundTaskIntermittently(): Job =
launch {
repeat(1000) {
delay(2000)
backGroundTask.cancelAndJoin()
}
}
I already have an event loop in my code so I just wanna use that to trigger new job creation. startListeningForRestartEvent will continuously listen for a restart event and create a new someBackGroundTask when it receives true.
Questions:
Is there any better way to achieve this?
Is it ok to do launch { restartChannel.send(true) } inside invokeOnCompletion?
Will the NonCancellable coroutine help me here?

Why won't my UI update while a background task is running?

I have this code that should show a counter while a background task is running:
#Composable fun startIt() {
val scope = rememberCoroutineScope()
val running = remember { mutableStateOf(false) }
Button({ scope.launch { task(running) } }) {
Text("start")
}
if (running.value) Counter()
}
#Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(100.milliseconds)
count.value += 1
}
}
Text(count.toString())
}
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
coroutineScope {
launch {
// some code that blocks this thread
}
}
running.value = false
}
If I understand correctly, the coroutineScope block in task should unblock the main thread, so that the LaunchedEffect in Counter can run. But that block never gets past the first delay, and never returns or gets cancelled. The counter keeps showing 0 until the task finishes.
How do I allow Compose to update the UI properly?
coroutineScope doesn't change the coroutine context, so I think you're launching a child coroutine that runs in the same thread.
The correct way to synchronously do blocking work in a coroutine without blocking the thread is by using withContext(Dispatchers.IO):
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
withContext(Dispatchers.IO) {
// some code that blocks this thread
}
running.value = false
}
If the blocking work is primarily CPU-bound, it is more appropriate to use Dispatchers.Default instead, I think because it helps prevent the backing thread pool from spawning more threads than necessary for CPU work.
This was a small issue of the way count was being modified, and not of coroutines. To fix your code, the remember for count in Counter() needed to be updated to :
#OptIn(ExperimentalTime::class)
#Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count.value += 1
}
}
Text(count.value.toString())
}
Remember can be done with delegation to remove the need of using the .value such as:
#OptIn(ExperimentalTime::class)
#Composable private fun Counter() {
var count by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count += 1
}
}
Text(count.toString())
}
Compose does coroutines slightly differently than Kotlin would by default, this is a small example that shows a bit more of how Compose likes Coroutines to be done:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Compose_coroutinesTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
BaseComponent()
}
}
}
}
}
// BaseComponent holds most of the state, child components respond to its values
#Composable
fun BaseComponent() {
var isRunning by remember { mutableStateOf(false) }
val composableScope = rememberCoroutineScope()
val count = remember { mutableStateOf(0) }
Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: ${count.value}")
// Using the async context of a button click, we can toggle running off and on, as well as run our background task for incrementing the counter
ToggleCounter(isRunning) {
isRunning = !isRunning
composableScope.launch {
while (isRunning) {
delay(100L)
count.value += 1
}
}
}
}
}
// Accepting an onTap function and passing it into our button, allows us to modify state as a result of the button, without the button needing to know anything more
#Composable
fun ToggleCounter(isRunning: Boolean, onTap: () -> Unit) {
val buttonText = if (isRunning) "Stop" else "Start"
Button(onClick = onTap) {
Text(buttonText)
}
}

Should I use rememberCoroutineScope or viewModelScope? Which one is right for my scenario?

In my example, I need to perform a process and if it is successful, the app should navigate to the next page.
The first approach uses viewmodel scope on a non suspend function and then uses a callback to let the UI know that the process is complete. The second approach is declaring a coroutine scope in the UI level and then making the viewmodel a suspend function.
Which would be the right approach for my scenario?
What's the advantage of each approach over the other?
Is there a particular scenario where one approach is more applicable than the other?
First approach:
UI:
Button(
onClick = {
vm.process(
onSuccess = { navcontroller.Navigate("nextpage") },
onFail = { errorMessage.value = it }
)
}
)
ViewModel:
fun process() {
viewmodelScope.launch {
val err = process()
if(err.isBlank()) {
onSuccess()
} else {
onFail(err)
}
}
}
Second approach?
UI:
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
val err = vm.process()
if(err.isBlank()) {
navController.navigate("nextPage")
} else {
errorMessage.value = err
}
}
})
ViewModel:
suspend fun process() : String {
val err = process()
if(err.isBlank()) {
onSuccess()
} else {
onFail(err)
}
}
Qs: What's the difference between rememberCoroutineScope() & viewmodelscope?
The rememberCoroutineScope() is tied to the lifecycle of the composable & the ViewModelScope is the same as the lifecycle scope, the only difference is that the coroutine in this scope will live as long as the view model is alive.
i.e
On orientation change, the coroutines in ViewModelScope will live but the lifecycle scope's coroutines will die/end.

Cancel current co routine kotlin

How do i cancel the current co routine if the current coroutine is active ?
Here is the code
fun coRoutine2(interval: Long) {
val coroutineScope = CoroutineScope(Dispatchers.IO)
if (coroutineScope.isActive) {
coroutineScope.cancel()
} else {
coroutineScope.launch {
for (i in 1..progressBar.max) {
delay(interval)
progressBar.progress = i
println(i)
}
}
}
}
If you want to cancel couroutine you should cancel the Job object returned when you call launch method
val job = coroutineScope.launch {
for (i in 1..progressBar.max) {
delay(interval)
progressBar.progress = i
println(i)
}
}
job.cancel()
see more examples in official documentation

Fan-out / fan-in - closing result channel

I'm producing items, consuming from multiple co-routines and pushing back to resultChannel. Producer is closing its channel after last item.
The code never finishes as resultChannel is never being closed. How to detect and properly finish iteration so hasNext() return false?
val inputData = (0..99).map { "Input$it" }
val threads = 10
val bundleProducer = produce<String>(CommonPool, threads) {
inputData.forEach { item ->
send(item)
println("Producing: $item")
}
println("Producing finished")
close()
}
val resultChannel = Channel<String>(threads)
repeat(threads) {
launch(CommonPool) {
bundleProducer.consumeEach {
println("CONSUMING $it")
resultChannel.send("Result ($it)")
}
}
}
val iterator = object : Iterator<String> {
val iterator = resultChannel.iterator()
override fun hasNext() = runBlocking { iterator.hasNext() }
override fun next() = runBlocking { iterator.next() }
}.asSequence()
println("Starting interation...")
val result = iterator.toList()
println("finish: ${result.size}")
You can run a coroutine that awaits for the consumers to finish and then closes the resultChannel.
First, rewrite the code that starts the consumers to save the Jobs:
val jobs = (1..threads).map {
launch(CommonPool) {
bundleProducer.consumeEach {
println("CONSUMING $it")
resultChannel.send("Result ($it)")
}
}
}
And then run another coroutine that closes the channel once all the Jobs are done:
launch(CommonPool) {
jobs.forEach { it.join() }
resultChannel.close()
}