I have a job inside my AndroidViewModel
class. Job is triggered by viewModelScope.launch
. Job is a long running process which return result by lambda functions. According to requirement If user want to cancel the job while remaining in the scope on button click it should cancel the job. The problem is when I cancel the job, process is still running in the background and it is computing the background task. Below is my ViewModelClass with its job and cancel function.
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
class SelectionViewModel(val app: Application) : AndroidViewModel(app) {
private var mainJob: Job? = null
private var context: Context? = null
fun performAction(
fileOptions: FileOptions,
onSuccess: (ArrayList<String>?) -> Unit,
onFailure: (String?) -> Unit,
onProgress: (Pair<Int, Int>) -> Unit
) {
mainJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
kotlin.runCatching {
while (isActive) {
val mOutputFilePaths: ArrayList<String> = ArrayList()
// Long running Background task
.. progress
OnProgress.invoke(pair)
// resul success
onSuccess.invoke(mOutputFilePaths)
}
}.onFailure {
withContext(Dispatchers.Main) {
onFailure.invoke(it.localizedMessage)
}
}
}
}
}
fun cancelJob() {
mainJob?.cancel()
}
}
Here it is I am initiating my ViewModel
val viewModel: SelectionViewModelby lazy {
ViewModelProviders.of(this).get(SelectionViewModel::class.java)
}
and when I start the job I call the following method
viewModel.performAction(fileOptions,{success->},{failure->},{progress->})
When I want to cancel the task. I call the following method.
viewModel.cancelJob()
Problem is even after canceling the job I am still receiving the progress as it is being invoked. This means job has not been canceled. I want to implement the correct way to start and cancel the job while remaining in the viewmodel scope.
So what is the proper way to implement the viewmodel to start and cancel the job?
In order to cancel the job you have to have a suspending function call.
This means that if your job has code like
while (canRead) {
read()
addResults()
}
return result
this can never be cancelled the way you wish it to be cancelled.
there are two ways you can cancel this code
a) add a delay function (this will check for cancelling and cancel your job)
b) (which in the above case is the correct way) periodically add a yield() function
so the above code should look like this:
while(canRead) {
yield()
read()
addResults()
}
return result
edit: some further explanations are probably necessary to make this clear
just because you run something withContext, does not mean that coroutines can stop or break it at any time
what coroutines do is basically change the old way of doing things with callbacks and replace it with suspending functions
what we used to do for complex calculations was start a thread ,which would execute the calculations and then get a callback with the results.
at any point you could cancel the thread and the work would stop.
cancelling coroutines is not the same
if you cancel a coroutine what you basically do is tell it that the job is cancelled , and at the next opportune moment it should stop
but if you don't use yield() delay() or any suspending function such an opportune moment will never arrive
it is the equivalent of running something like this with threads
while(canRead && !cancelled) {
doStuff
}
where you would manually set the cancelled flag, if you set it but didn't check it in your code , it would never stop
as a side note, be careful because right now you have a big block of calculations running code, this will run on one thread because you never called a suspending function. When you add the yield() call , it could change threads or context (within what you defined ofc) so make sure it is thread safe