
CoroutineScope is an interface with a single property: coroutineContext.interface CoroutineScope { val coroutineContext: CoroutineContext }
class SomeClass : CoroutineScope { override val coroutineContext: CoroutineContext = Job() fun onStart() { launch { // ... } } }
CoroutineScope methods like cancel or ensureActive in such a class. Even accidentally, someone might cancel the whole scope, therefore coroutines will not start anymore. Instead, we generally prefer to hold a coroutine scope as an object in a property and use it to call coroutine builders.class SomeClass { val scope: CoroutineScope = ... fun onStart() { scope.launch { // ... } } }
CoroutineScope factory function[^208_1]. It creates a scope with the provided context (and an additional Job for structured concurrency if no job is already part of the context).public fun CoroutineScope( context: CoroutineContext ): CoroutineScope = ContextScope( if (context[Job] != null) context else context + Job() ) internal class ContextScope( context: CoroutineContext ) : CoroutineScope { override val coroutineContext: CoroutineContext = context override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)" }
scope or backgroundScope.class CacheRefreshService( private val userService: UserService, private val newsService: NewsService, private val backgroundScope: CoroutineScope, ) { fun refresh() { scope.launch { userService.refresh() newsService.refresh() } } } class DataSyncManager( backgroundScope: CoroutineScope, ) { init { backgroundScope.launch { while (isActive) { retryOnFailure { syncDataWithServer() delay(syncIntervalMillis) } } } } // ... }
- have a specified dispatcher;
- include
SupervisorJobso all coroutines started in this scope are not cancelled if one fails; - optionally, include some
CoroutineExceptionHandlerto set custom logging, to respond with proper error codes, or to send dead letters[^208_2].
Dispatchers.Default, which is a great choice if we are sure that we will not make any blocking calls;Dispatchers.IO, which is a great choice if we plan to block threads, only remember to uselimitedParallelismon services.Dispatchers.IO.limitedParallelism(n), which is a great choice if you plan to block threads, or you don’t plan this, but you want to be prepared if some developer blocks threads by accident (this option is nearly as CPU-efficient asDispatchers.Default, but it is much safer as accidental thread-blocking costs us much less);- a dispatcher made from
Executors, which is a good choice if you want to configure your threads in ways that are not possible with Kotlin Coroutines dispatchers (like setting priorities, names, types, etc.).
SupervisorJob and a custom dispatcher with a limited number of threads, and we set a custom exception handler to log all unhandled exceptions.@Configuration public class CoroutineScopeConfiguration { @Bean fun coroutineDispatcher(): CoroutineDispatcher = Dispatchers.IO.limitedParallelism(50) @Bean fun coroutineExceptionHandler( monitoringService: MonitoringService, ): CoroutineExceptionHandler { val logger = LoggerFactory.getLogger("DefaultHandler") CoroutineExceptionHandler { _, throwable -> logger.error("Unhandled exception", throwable) monitoringService.reportError(throwable) } } @Bean fun coroutineScope( coroutineDispatcher: CoroutineDispatcher, coroutineExceptionHandler: CoroutineExceptionHandler, ) = CoroutineScope( SupervisorJob() + coroutineDispatcher + coroutineExceptionHandler ) }
class DocumentsUpdater( val documentsFetcher: DocumentsFetcher, ) { private val disp = Dispatchers.IO.limitedParallelism(50) private val scope = CoroutineScope(SupervisorJob() + disp) private val documentsDir = File("documents") fun updateBooks() { scope.launch { val documents = documentsFetcher.fetch() documents.forEach { document -> val file = File(documentsDir, document.name) file.writeBytes(document.content) } } } }
- use
Dispatchers.Main.immediateas the default dispatcher; - use
SupervisorJobto prevent all coroutines being cancelled if one fails; - cancel all coroutines when the view or view model is destroyed;
- probably use a
CoroutineExceptionHandlerto set default ways to handle exceptions.
abstract class BaseViewModel : ViewModel() { private val _failure = MutableSharedFlow<Throwable>() val failure: SharedFlow<Throwable> = _failure private val context = Dispatchers.Main.immediate + SupervisorJob() + CoroutineExceptionHandler { _, throwable -> _failure.tryEmit(throwable) } protected val scope = CoroutineScope(context) override fun onCleared() { context.cancelChildren() } }
viewModelScope (needs lifecycle-viewmodel-ktx from androidx.lifecycle version 2.2.0 or higher) or lifecycleScope (needs lifecycle-runtime-ktx from androidx.lifecycle version 2.2.0 or higher). How they work is nearly identical to what we've just constructed: they use Dispatchers.Main.immediate and SupervisorJob, and they cancel the job when the view model or lifecycle owner gets destroyed.// Implementation from lifecycle-viewmodel-ktx version 2.4.0 public val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? = this.getTag(JOB_KEY) if (scope != null) { return scope } return setTagIfAbsent( JOB_KEY, CloseableCoroutineScope( SupervisorJob() + Dispatchers.Main.immediate ) ) } internal class CloseableCoroutineScope( context: CoroutineContext ) : Closeable, CoroutineScope { override val coroutineContext: CoroutineContext = context override fun close() { coroutineContext.cancel() } }
viewModelScope and lifecycleScope is convenient and recommended if we don't need any special context as a part of our scope (like CoroutineExceptionHandler). This is why this approach is chosen by most Android applications.class ArticlesListViewModel( private val produceArticles: ProduceArticlesUseCase, ) : ViewModel() { private val _progressVisible = MutableStateFlow(false) val progressBarVisible: StateFlow<Boolean> = _progressVisible private val _articlesListState = MutableStateFlow<ArticlesListState>(Initial) val articlesListState: StateFlow<ArticlesListState> = _articlesListState fun onCreate() { viewModelScope.launch { _progressVisible.value = true val articles = produceArticles.produce() _articlesListState.value = ArticlesLoaded(articles) _progressVisible.value = false } } }
CoroutineScope factory function. We've learned that we should use SupervisorJob to prevent all coroutines being cancelled if one fails. We can set a custom dispatcher and a custom exception handler to handle exceptions. We can also cancel all coroutines by calling cancelChildren in the context. Finally, we've learned that we can use viewModelScope and lifecycleScope in Android applications.[^208_2]: This is a popular microservices pattern that is used when we use a software bus, like in Apache Kafka.