Search code examples
androidviewmodelkotlin-coroutinesandroid-viewmodel

AndroidViewModel cancel job on demand


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?


Solution

  • 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