article banner

Constructing a coroutine scope

This is a chapter from the book Kotlin Coroutines. You can find it on LeanPub or Amazon.

In previous chapters, we've discussed all the components needed to construct a proper scope. Now it is time to summarize this knowledge and see how it is typically used.

CoroutineScope factory function

CoroutineScope is an interface with a single property coroutineContext.

interface CoroutineScope { val coroutineContext: CoroutineContext }

Therefore, we can make a class implement this interface and just directly call coroutine builders in it.

class SomeClass : CoroutineScope { override val coroutineContext: CoroutineContext = Job() fun onStart() { launch { // ... } } }

However, this approach is not very popular. On one hand, it is convenient; on the other, it is problematic that in such a class we can directly call other CoroutineScope methods like cancel or ensureActive. Even accidentally, someone might cancel the whole scope, and 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 { // ... } } }

The easiest way to create a coroutine scope object is by using the CoroutineScope factory function1. It creates a scope with 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)" }

Constructing a background scope

When you need to start an asynchronous coroutine, you need a scope. Using a scope from a suspending function bounds this coroutine to the caller coroutine. If you want to start a new process, you need a custom scope. This scope is typically injected via the constructor, and we typically name it 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) } } } } // ... }

So, how should we construct such a scope? Typically, we want it to:

  • have a specified dispatcher;
  • include SupervisorJob to not cancel all coroutines started on this scope if one fails;
  • optionally some CoroutineExceptionHandler to set custom logging, to respond with proper error codes, or to send dead letters2.

Regarding the dispatcher, the most popular options are:

  • Dispatchers.Default, which is a great choice if we are sure that we do not make any blocking calls;
  • Dispatchers.IO with a limited number of threads, which is a good choice if we might have some blocking calls. This option can be both used if we plan to use this scope for blocking calls, and if we do not need that, but we are worried that in some coroutines we might accidentally make some blocking calls (then we pay a price of having more threads than we need, but an accidental blocking call will cause less harm);
  • 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, type, etc.).

Here is an example configuration for a Spring Boot application. We use SupervisorJob, 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() { val logger = LoggerFactory.getLogger("DefaultHandler") CoroutineExceptionHandler { _, throwable -> logger.error("Unhandled exception", throwable) } } @Bean fun coroutineScope( coroutineDispatcher: CoroutineDispatcher, coroutineExceptionHandler: CoroutineExceptionHandler, ) = CoroutineScope( SupervisorJob() + coroutineDispatcher + coroutineExceptionHandler ) }

A similar scope can also be used to start coroutines for each request in a web server. Though, that is typically done by the framework we use (both Spring Boot and Ktor have their own ways to start coroutines for each request).

Of course, a scope can also be created in the class that needs it. Dependency injection is not mandatory, but it is very useful for testing and for making the code more modular. Here is an example of how to construct a scope in a class:

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) } } } }

Constructing a scope on Android

When we construct a scope on Android, we typically want to:

  • use Dispatchers.Main.immediate as the default dispatcher;
  • use SupervisorJob to not cancel all coroutines if one fails;
  • cancel all coroutines when the view or view model is destroyed;
  • probably use some CoroutineExceptionHandler to set default ways to handle exceptions.

This is what a scope construction in a base view model might look like:

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() } }

In modern Android applications, instead of defining your own scope, you can also use viewModelScope (needs androidx.lifecycle:lifecycle-viewmodel-ktx version 2.2.0 or higher) or lifecycleScope (needs androidx.lifecycle:lifecycle-runtime-ktx 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() } }

Using viewModelScope and lifecycleScope is convenient and recommended if we do not 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 } } }

Summary

This chapter showed how to construct a coroutine scope in different environments. Such a scope can be used to start coroutines and manage their lifecycle. We've seen that the most common way to construct a scope is by using the CoroutineScope factory function. We've learned, that we should use SupervisorJob to not cancel all coroutines 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 on the context. Finally, we've learned that in Android applications we can use viewModelScope and lifecycleScope.

1:

A function that looks like a constructor is known as a fake constructor. This pattern is explained in Effective Kotlin Item 32: Consider factory functions instead of secondary constructors.

2:

This is a popular microservices pattern that is used when we use a software bus, like in Apache Kafka.