In case of Android, we can cancel a job and its children in onDestroy. Is there a way to pause it (e.g. in onStop) and resume it (e.g. in onStart)? Or any plan to implement it?
Not sure, but I have idea like this:
class MyFragment : Fragment() {
...
private val parentJob = Job() // cancel on destroy
private var stoppedJob: Job? = null // active on stop
...
fun onStart() {
super.onStart()
stoppedJob?.cancel()
}
fun onStop() {
super.onStop()
stoppedJob = Job()
}
fun onDestroy() {
super.onDestroy()
parentJob.cancel()
}
private fun showSomething(id: Long) = launch(UI + parentJob) {
val result = asyncSomething(id).await()
stoppedJob?.join()
updateUiBasedOnSomething(result)
}
...
}
What you exactly want to happen while it is paused? What kind of asynchronous activities you are concerned about?
If you've sent some network network request to a server, then it does not make much sense "to pause" it, because there is nothing you are can really do about it -- it is already is flight and is being processed by the server.
However, if you doing some UI animations with coroutines or something like that, then pausing them indeed makes sense. In the latter case, I'd suggest to implement a dedicated CoroutineDispatcher that is tied to your activity life cycle. It can queue all the work while activity is paused to resume it when it is restarted.
However, if you doing some UI animations with coroutines or something like that, then pausing them indeed makes sense.
Yes, it's what I meant, there's no problem with the HTTP response, but I want to delay displaying/toasting/animating something based on that response until the fragment/activity started (in case it is stopped).
Thank you for this thread, I am looking for something like that.
In java I use my implementation of Promise/Deferred, so in Activity.onStop I create Deferred<Void> whenActivityRunning and onStart I resolve this deferred. Usage:
``java
asyncOperation.run(args)
.thenPromise(result -> whenActivityRunning.then(() -> result))
.thenVoid(result -> updateUiOrSmthElse(result));
````
As I undestand, current suggestion is implement a dedicatedCoroutineDispatcher `, does it?
I tried to implement simple pausable dispatcher:
class PausableDispatcher(private val handler: Handler): CoroutineDispatcher() {
private val queue: Queue<Runnable> = LinkedList()
private var isPaused: Boolean = false
@Synchronized override fun dispatch(context: CoroutineContext, block: Runnable) {
println("dispatch")
if (isPaused) {
queue.add(block)
} else {
handler.post(block)
}
}
@Synchronized fun pause() {
println("pause")
isPaused = true
}
@Synchronized fun resume() {
println("resume")
isPaused = false
runQueue()
}
private fun runQueue() {
queue.iterator().let {
while (it.hasNext()) {
val block = it.next()
it.remove()
handler.post(block)
}
}
}
}
usage:
class MainActivity : Activity() {
private val dispatcher = PausableDispatcher(Handler(Looper.getMainLooper()))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button = findViewById<Button>(R.id.button1)
button.setOnClickListener {
launch(dispatcher) {
suspendFunc()
delay(1000)
suspendFunc()
delay(1000)
suspendFunc()
}
}
}
override fun onPause() {
super.onPause()
dispatcher.pause()
}
override fun onResume() {
super.onResume()
dispatcher.resume()
}
override fun onDestroy() {
super.onDestroy()
dispatcher.cancel()
}
}
If I "minimize" my application after first suspendFunc call, next call pushes to queue.
When I "restore" my application, it continues execution.
I am not sure about "dispatcher.cancel", and about implementation details, but it seems to work. Any suggestions?
I'm closing this issue in favor of #258. Having thought about the issue I don't think that solving "pausing" problem on a dispatcher level is a right way to do it. It is better to explicitly write code that suspends coroutine until the desired state is reached.
I found one use case where is pausable dispatcher very handy. I will try to explain as a simplified model. Let's say we have some global notification display. We have to display only one at the time. There could be many requests at the time. Also, the user is able to pause notifications at any time. The code could be as simple as that:
private val notifications = Channel<Notification>(UNLIMITED)
suspend fun enqueueNotification(notification: Notification) {
notifications.send(notification)
}
suspend fun pause() { pauseable.pause() }
suspend fun resume() { pauseable.resume() }
private fun launchDispacher() {
withContext(pauseable) {
for (notification in notifications) {
val view = prepareView(notification)
showInAnimation(view) //suspension point.
delay(5000) //suspension point
showOutAnimation(view) //suspension point.
}
}
}
A user has to be able to pause here. Dont ask why :) I am ok to pause dispacher at any suspension point.
@neworld
Why not cancel Job on pause and start a new Job on resume?
Because of this:
showInAnimation(view) //suspension point.
delay(5000) //suspension point
showOutAnimation(view) //suspension point.
Sometimes new job should start from begining, sometimes new job need to close old notification first. And there is very simplified version. Real one has more suspension points.
I'm trying to understand your real use case, your last code can be rewritten:
try {
showInAnimation(view) //suspension point.
delay(5000) //suspension point
} finally {
withContext(NonCancellable) {
showOutAnimation(view) //suspension point.
}
}
alternatively you can use a Mutex to pause/resume the job.
Actually, it can't be rewrite in this way, because if the user pauses notification while one is shown, it should be visible as long as the user doesn't resume. Mutex could do the job as well:
for (notification in notifications) {
val view = prepareView(notification)
pauseable.withLock { showInAnimation(view) }
delay(5000) //suspension point
pauseable.withLock { showOutAnimation(view) }
}
now we can use
viewLifecycleOwner.lifecycleScope.launchWhenResumed { ... }
it's working like pausing
Launches and runs the given block when the Lifecycle controlling this LifecycleCoroutineScope is at least in Lifecycle.State.RESUMED state.
The returned Job will be cancelled when the Lifecycle is destroyed.
LifecycleCoroutineScope.launchwhenresumed
Really? Is it paused everytime the lifecycle pauses, continued when the lifecycle resumes, paused again when the lifecycle pauses again?
I have checked and it's working as I expected. You can check it by yourself.
lifecycleScope.launchWhenResumed
A little bit late :) I have tested and used it., it is working for my case. Thanks a lot @morder this what I am searching for.
Most helpful comment
I'm closing this issue in favor of #258. Having thought about the issue I don't think that solving "pausing" problem on a dispatcher level is a right way to do it. It is better to explicitly write code that suspends coroutine until the desired state is reached.