Constructing coroutine scope
We already have all the tools we need to construct a proper coroutine scope. Now it is time to summarize this knowledge and see how it is typically used. We will see two common examples: one for Android, and one for backend development.
CoroutineScope factory function
CoroutineScope is an interface with a single property
Therefore we can make a class implement this interface and just directly call coroutine builders in it.
However, this approach is not very popular. On one hand, it is convenient, on the other it is strange that in such a class we can also directly call other
CoroutineScope methods like
ensureActive. Instead, we generally prefer holding coroutine scope as an object in a property and use it to call coroutine builders.
The easiest way to create a coroutine scope object is by using the
CoroutineScope factory function1. It creates a scope with provided context (and additional
Job for structured concurrency if no job is part of the context already).
Constructing a scope on Android
In most Android applications, we use an architecture that is a descendant of MVC: currently mainly MVVM or MVP. On those architectures, we extract business logic into objects called ViewModels or Presenters. This is where coroutines are generally started. On other layers, like in Use Cases or Repositories, we generally are just using suspending functions. The coroutines might also be started on Fragments or Activities. Regardless of where coroutines will be started on Android, the way how they would be constructed will most likely be the same. Let's take a
MainViewModel as an example: let's say it needs to fetch some data in
onCreate (that is called when a user enters the screen). This data fetching needs to happen in a coroutine which needs to be called on some scope. We will construct a scope on the
BaseViewModel, to have it defined once for all view models. So in the
MainViewModel we can just use the
scope property from
Time to define a context for this scope. Given that many functions on Android need to be called on the Main thread,
Dispatchers.Main is considered the best option as the default dispatcher. Our context will be built starting from it.
Second, we need to make our scope cancellable. It is a common feature to cancel all the unfinished processes once a user exits a screen and
onDestroy is called. To make our scope cancellable, we need it to have some
Job (we do not really need to add it, because if we don't it will be added by
CoroutineScope function anyway, but it is more explicit this way). Then we can cancel it in
Even better, it is a common practice to not cancel the whole scope, but just its children. Thanks to that, as long as this view model is active, new coroutines can start on its scope.
We also want different coroutines started on this scope to be independent. Just because there was an exception when loading user data, it should not stop us from seeing the news. To have such independence, we should use
SupervisorJob instead of just
The last important functionality is the default way to handle uncaught exceptions. On Android, we often define what should happen in case of different kinds of exceptions. If we receive a
401 Unauthorized from an HTTP call, we might open the login screen. On a
503 Service Unavailable, we might show a server problem message. In other cases, we might show dialogs, snackbars, or toasts. We often define those exception handles once, for instance in some
BaseActivity, and then pass it to view models (often via constructor). Then, we can use
CoroutineExceptionHandler to call this function in case of an unhandled exception.
An alternative would be to hold exceptions as live data property, that is observed on the
BaseActivity or another view element.
In modern Android applications, instead of defining your own scope, you can also use
2.2.0 or higher) or
2.2.0 or higher). The way how they work is nearly identical to what we've just constructed. They use
SupervisorJob and cancel the job when the view model or lifecycle owner gets destroyed. It is enough for many small projects, but in bigger ones, we need more contexts (for instance
CoroutineExceptionHandler) and more control over the scope (mainly for unit testing).
Constructing a coroutine on the backend
Many backend frameworks have built-in support for suspending functions. Spring Boot allows suspending controller functions. In Ktor, all handlers are suspending by default. Thanks to that, we rarely need to create scope ourselves. Although assuming that we do (maybe because we need to start a task, or work with an older version of Spring), what we most likely need is:
- a custom dispatcher with a pool of threads (or
SupervisorJobto make different coroutines independent,
- probably some
CoroutineExceptionHandlerto respond with proper error codes, send dead letters2 or log the problems.
Such scope is most often injected into classes via the constructor. Thanks to that, the scope can be defined once to be used on many classes, and it can be easily replaced with a different scope for testing purposes.
I hope that after this chapter you will know how to construct scope in most typical situations. This was an important puzzle for using coroutines in real-life projects. Enough for many small and simple applications, but for those that are more serious, we still need to cover two more topics - proper synchronization and testing.
The function looking like a constructor is known as fake constructor. This pattern is explained in the Effective Kotlin Item 33: Consider factory functions instead of constructors.
It is a popular microservices pattern used when we use software bus, like Apache Kafka.