Что происходит при отмене coroutine и как родительская coroutine узнает об отмене дочерней
Что проверяют
Понимаете ли вы, что отмена корутин кооперативная, и знаете ли правила структурной конкурентности: кто кого валит при отмене, а кто переживает её молча. На Android это не теория, а причина утечек и падений после закрытия экрана.
Как отвечать
cancel() не останавливает код, а переводит Job в состояние отмены и ставит флаг. Дальше:
- на ближайшей точке приостановки, которая умеет отменяться (
delay,yield,withContext,await), бросаетсяCancellationException; - если корутина считает в цикле без suspend-вызовов, она не остановится, пока сама не проверит
isActiveили не позовётensureActive(); - отмена распространяется вниз: дочерние корутины отменяются, а родитель остаётся в состоянии отмены, пока не завершится последний ребёнок.
Про связь с родителем главное такое. CancellationException от ребёнка родитель считает штатным завершением и сам не падает. Любое другое исключение ребёнок пробрасывает наверх, родитель отменяет себя и остальных детей, поэтому один упавший запрос гасит весь viewModelScope. Меняет это SupervisorJob или supervisorScope, где дети падают независимо друг от друга. Узнать о судьбе конкретного ребёнка можно через job.invokeOnCompletion или join().
Где спотыкаются
Ловят Exception общим обработчиком внутри корутины и глотают CancellationException. Корутина после этого продолжает работать, отмена не срабатывает, экран давно закрыт. Ловить нужно точечно либо пробрасывать CancellationException дальше.
Второе: пишут suspend-вызов в finally для уборки и удивляются, что он не выполняется. В отменённой корутине любая точка приостановки сразу бросает исключение, для такого кода нужен withContext(NonCancellable).
Третье: думают, что cancel() у ребёнка отменит родителя. Вверх отмена не идёт. Зато cancel() у скоупа завершает его насовсем: отменённый Job переиспользовать нельзя, скоуп создают заново, а не чинят.