Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to reuse a Job instance?

I'm exploring the use of co-routines in the context of Android UI thread. I implemented contextJob as described in the Coroutines Guide UI. Background work is stared from GUI and I want to re-start it on every click (stop the currently running one and start it again).

But a job once canceled cannot be reused so even creating a child-job:

 val job = Job(contextJob)

and cancelling it does not help because it has to be-reassigned.

Is there a way to reuse a Job instance?

like image 394
atok Avatar asked Mar 16 '17 09:03

atok


1 Answers

A Job has a very simple life-cycle by design. Its "Completed" state is final, very much similar to the "Destroyed" state of the Android Activity. So, a parent Job is best to be associated with an Activity, as explained in the guide. You should cancel a parent job if and only if the activity is destroyed. Because a destroyed activity cannot be reused, you'll never run into the need to reuse its job.

The recommended approach to starting the work on each click is by using actors, because they help you avoid unneccesary concurrency. The guide shows how to start them on each click, but it does not show how to cancel a currently running action.

You will need a fresh instance of Job in a combination with withContext to make a block of code cancellable separately from everything else:

fun View.onClick(action: suspend () -> Unit) {
    var currentJob: Job? = null // to keep a reference to the currently running job
    // launch one actor as a parent of the context job
    // actor prevent concurrent execution of multiple actions
    val eventActor = actor<Unit>(contextJob + UI, capacity = Channel.CONFLATED) {
        for (event in channel) {
            currentJob = Job(contextJob) // create a new job for this action
            try {
                // run an action within its own job
                withContext(currentJob!!) { action() }
            } catch (e: CancellationException) {
                // we expect it to be cancelled and just need to continue
            }
        }
    }
    // install a listener to send message to this actor
    setOnClickListener {
        currentJob?.cancel() // cancel whatever job we were doing now (if any)
        eventActor.offer(Unit) // signal to start next action when possible
    }
}

An actor is always active until its parent job (attached to an activity) is cancelled. An actor waits for clicks and starts an action on each click. However, each invocation of an action is wrapped into its own Job using withContext block, so that it can be cancelled separately from its parent job.

Note, that this code gracefully works for actions that are non-cancellable or just take some time to cancel. An action might need to cleanup its resources when it is cancelled, and, because this code uses an actor, it ensures that the cleanup of the previous action is finished before the next one is started.

like image 153
Roman Elizarov Avatar answered Oct 17 '22 21:10

Roman Elizarov