Following coroutine’s best practices, you might need to inject an application-scoped
CoroutineScope in some classes to launch new coroutines that follow the app lifecycle or to make certain work outlive the caller’s scope.
In this article, you’ll learn how to create an application-scoped
CoroutineScope using Hilt, and how to inject it as a dependency. To further improve the way we work with Coroutines, we’ll see how to inject the different
CoroutineDispatchers and replace their implementations in tests.
Manual dependency injection
To create an application-scoped
CoroutineScope following dependency injection (DI) best practices manually without any library, you’d typically add a new variable to your application class with an instance of a
CoroutineScope. The same instance would be manually passed around when creating other objects.
Since there isn’t a reliable way to know when the
Application is destroyed in Android, you don’t need to call
applicationScope.cancel() manually as the scope and all ongoing work will be destroyed when the application process finishes.
A better option for doing this manually is to create an
ApplicationContainer class that holds the application-scoped types. This helps with separation of concerns since these Container classes are responsible for:
- handling the logic of how to build certain types,
- holding container-scoped types instances, and
- returning instances of scoped and unscoped types.
Note: A container always returns the same instance of a scoped type, and always returns a different instance for unscoped types. Scoping types to containers is costly since the scoped object stays in memory until the component is destroyed, so only scope what’s really needed.
ApplicationDiContainer example above, all types were scoped. If MyRepository didn’t need to be scoped to the application, we’d have:
Using Hilt in your app
Hilt generates what you can see in
ApplicationDiContainer (and more!) at compile time using annotations. Moreover, Hilt provides containers for most Android framework classes not only for the
To set up Hilt in your app and create the container for the
Application class, annotate your
Application class with
With this, the application DI container is ready to be used. We just need to let Hilt know how to provide instances of different types.
Note: In Hilt, Container classes are referenced as Components. The container associated with the
Applicationclass is called
SingletonComponent. Check out the list of all available Hilt components.
Construction injection is the easiest way to let Hilt know how to provide instances of a type if we have access to the constructor of a class as we only need to annotate the constructor with
This lets Hilt know that in order to provide an instance of the
MyRepository class, an instance of
CoroutineScope needs to be passed as a dependency. Hilt generates code at compile time to make sure dependencies are satisfied and passed in when creating an instance of a type or give errors in case it doesn’t have enough information.
@Singleton is used to scope this class to the
At this point, Hilt doesn’t know how to satisfy the
CoroutineScope dependency because we haven’t told Hilt how to do that. The following sections will explain how we can let Hilt know what to pass as a dependency.
Note: Hilt provides a different annotation to scope types to the different Hilt available components. Check out the list of all available component scopes.
A binding is a commonly-used term in Hilt that denotes the information Hilt knows about how to provide instances of a type as a dependency. We could say that we added a binding to Hilt with the
@Inject annotation of the code snippet above.
Bindings flow through Hilt’s components hierarchy. Bindings that are available in the
SingletonComponent are also available in the
Bindings for unscoped types (an example of this could’ve been the
MyRepository code above if it wasn’t annotated with
@Singleton), are available in all Hilt components. Bindings that are scoped to a component, such as
MyRepository that is annotated with
@Singleton, are available to the scoped component and the components below it in the hierarchy.
Providing types with modules
As mentioned above, we need to let Hilt know how to satisfy the
CoroutineScope dependency. However,
CoroutineScope is an interface type that comes from an external library, so we cannot use constructor injection as we did before with the
MyRepository class. The alternative is letting Hilt know what code to run when providing the instance of a type using Modules:
@Provides method is annotated with
@Singleton to make Hilt always return the same instance of that
CoroutineScope. This is because any work that needs to follow the application lifetime should be created using the same instance of a
CoroutineScope that follows the
Hilt modules are annotated with
@InstallIn that indicates in which Hilt component (and components below in the hierarchy) the binding is installed. In our case, as the application
CoroutineScope is needed by
MyRepository which is scoped to the
SingletonComponent, this binding needs to be installed in the
SingletonComponent as well.
In Hilt jargon, we could say that we added a
CoroutineScope binding, as now, Hilt knows how to provide instances of
However, the code snippet above could be improved. Hardcoding dispatchers is a bad practice in coroutines, we should inject them to make them configurable and make testing easier. Following the previous code, we can create a new Hilt module to let it know which Dispatcher to inject for each case: main, default, and IO.
Providing implementations for CoroutineDispatcher
We have to provide different implementations for the same type:
CoroutineDispatcher. In other words, we need different bindings for the same type.
We use qualifiers to let Hilt know which binding, or implementation, to use each time. Qualifiers are just annotations that you and Hilt use to identify specific bindings. Let’s create one qualifier per
Then, these qualifiers annotate the different
@Provides methods to identify a specific binding in Hilt modules. The
@DefaultDispatcher qualifier annotates the method that returns the default dispatcher, and so on.
Note that these
CoroutineDispatchers don’t need to be scoped to the
SingletonComponent. Every time these dependencies are needed, Hilt calls the
@Provides method and returns the corresponding
Providing an application-scoped CoroutineScope
To get rid of the hardcoded
CoroutineDispatcher from our previous application-scoped
CoroutineScope code, we need to inject the Hilt-provided default dispatcher. For that, we can pass in the type we want to inject,
CoroutineDispatcher, using the corresponding qualifier,
@DefaultDispatcher, as a dependency in the method that provides the application
As Hilt has multiple bindings for the
CoroutineDispatcher type, we disambiguate it using the
@DefaultDispatcher annotation when
CoroutineDispatcher is used as a dependency.
A qualifier for ApplicationScope
Even though we don’t need multiple bindings for
CoroutineScope at the moment (this could change in the future if we ever need something like a
UserCoroutineScope), adding a qualifier to the application
CoroutineScope helps with readability when injecting it as a dependency.
MyRepository depends on this scope, it’s very clear which external scope uses as implementation:
Replacing Dispatchers for instrumentation tests
We said before that we should inject dispatchers to make testing easier and have full control over what’s happening. For instrumentation tests, we’d want to make Espresso wait for coroutines to finish.
Instead of creating a custom
CoroutineDispatcher with some Espresso Idling resource to make it wait for the coroutines to finish, we can take advantage of the AsyncTask API. Even though AsyncTask was deprecated in Android API 30, Espresso hooks into its thread pool to check for idleness. Therefore, any coroutine that should be executed in a background thread could be executed in the AsyncTask’s thread pool.
TestInstallIn API to make Hilt provide a different implementation of a type in tests. Similar to how we provided the different Dispatchers above, we can create a new file under the
androidTest package to provide different implementations for those Dispatchers.
With the code above, we’re making Hilt “forget” the
CoroutinesDispatchersModule used in production code in tests. That module will be replaced with
TestCoroutinesDispatchersModule which uses the Async Task’s thread pool for work that needs to happen in the background, and
Dispatchers.Main for work that needs to happen on the main thread which Espresso also waits for.
Warning: This implementation is obviously a hack that we’re not proud of. However, coroutines don’t currently integrate well with Espresso as there isn’t a way to know if a
CoroutineDispatcheris idle or not at the moment (Link to bug).
AsyncTask.THREAD_POOL_EXECUTORis the best alternative to use at the moment since Espresso doesn’t use Idling resources to check if this executor is idle, Espresso uses a different heuristic that takes into account what’s in the message queue. That makes it a better option than something like
IdlingThreadPoolExecutorwhich unfortunately considers the thread pool idle when a coroutine is suspended due to how coroutines are compiled down to a state machine.
For more information about testing, check out Hilt’s testing guide.
CoroutineScopeusing Hilt, inject it as a dependency, inject the different
CoroutineDispatcherinstances, and replace their implementations in tests.