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 on it.

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

However, this approach is not very popular because although it is convenient it is problematic that we can directly call other 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 { // ... } } }

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

Constructing a background scope

When you need to start an asynchronous coroutine, you need a scope. Using a scope from a suspending function binds 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 so all coroutines started in this scope are not cancelled if one fails;
  • optionally, include 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 will not make any blocking calls;
  • Dispatchers.IO, which is a great choice if we plan to block threads, only remember to use limitedParallelism on 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 as Dispatchers.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.).

Here is an example configuration for a Spring Boot application. We use 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 ) }

A similar scope can also be used to start coroutines for each request in a backend application. However, this is typically done by the framework we use (both Spring Boot and Ktor have their own ways of starting 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 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 prevent all coroutines being cancelled if one fails;
  • cancel all coroutines when the view or view model is destroyed;
  • probably use a 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 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() } }

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

Summary

This chapter has shown how to construct a coroutine scope in different environments. Such a scope can be used to start coroutines and manage their lifecycles. 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 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.

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.