Hi All, having some trouble with cancelling corout...
# coroutines
j
Hi All, having some trouble with cancelling coroutines in an Android app and wondering if you all could help: I have a class that launches coroutines and allows them to be cancelled when the Activity/Fragment they are called from is destroyed. However it is not working like I expect. When I back out of the fragment while the operation is running, the coroutine cancel does not take, and I get an NPE when trying to access a 
View
 that does not exist anymore.
Copy code
open class CoroutineLauncher : CoroutineScope {

    private val dispatcher: CoroutineDispatcher = Dispatchers.Main
    private val supervisorJob = SupervisorJob()
    override val coroutineContext: CoroutineContext
        get() = dispatcher + supervisorJob

    fun launch(action: suspend CoroutineScope.() -> Unit) = launch(block = action)

    fun cancelCoroutines() {
        supervisorJob.cancelChildren() //coroutineContext.cancelChildren() has same results
    }
}
here is the usage
Copy code
class MyFragment : Fragment {

  val launcher = CoroutineLauncher()

  fun onSomeEvent() {

    launcher.launch {

      val result = someSuspendFunction() // made suspend function by using withContext(<http://Dispatchers.IO|Dispatchers.IO>)

      if (!isActive) return

      // CAUSES CRASH
      myTextView.text = result.userText

    }

  }

  override fun onDestroyView() {
    super.onDestroyView()
    launcher.cancelCoroutines()
  }

}
I added log lines to ensure 
onDestroyView
 and 
cancelCoroutines
 are both being called before the crash. It is my understanding that
withContext
is cancellable and thus I shouldn’t even need to check for
isActive
. I feel like I’m missing something obvious, but I can’t figure out what it is
j
I would suspect this is because
myTextView
may not be available when you reach that line. If your coroutine is cancelled right before this call, it will still try to perform the assignment (cancellation is cooperative). Maybe you should not try to update the UI from the `launch`’s context. Use a
withContext
to ensure it runs on the UI thread. Another way would be to use
cancelAndJoin()
to make sure you wait for your coroutines to finish from
onDestroyView
j
@Joffrey You are definitely correct that
myTextView
is unavailable. My
CoroutineLauncher
uses
Dispatchers.Main
as its dispatcher so that should definitely run on the UI thread
Also I have logs showing that
cancelChildren
is called before
someSuspendFunction()
is even called. Which since
withContext
is ‘cooperative’ should mean that the Text View ix never even accessed
If anyone cares,
onSomeEvent
was being invoked after cancel was called. Since we call
cancelChildren
instead of
cancel
, the launcher does not refuse new Jobs, and since cancel already happened, the new coroutine runs like normal and crashes
j
Nicely spotted! Thanks for letting me know the final diagnostic, it's always interesting ☺️