When following the coroutine best practice , you may need to inject the application-level scoped CoroutineScope into some classes so that you can create a new coroutine with the same life cycle as the application, or create a new coroutine outside the scope of the caller. A new coroutine that can work.
Through this article, you will learn how to create application-level scoped CoroutineScope
through Hilt and how to inject it as a dependency. We will show in the example how to inject different CoroutineDispatcher
and replace its implementation in the test to further optimize the use of coroutines.
Manual dependency injection
Without using any libraries, follow the best practice of dependency injection (DI) to manually create a application-level scope of CoroutineScope
, usually add a CoroutineScope
instance variable in the Application class. When creating other objects, manually CoroutineScope
to these objects.
class MyRepository(private val externalScope: CoroutineScope) { /* ... */ }
class MyApplication : Application() {
// 应用中任何类都可以通过 applicationContext 访问应用级别作用域的类型
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val myRepository = MyRepository(applicationScope)
}
Since there is no reliable method in Android to obtain Application
destruction, and the application-level scope and any tasks being executed will be destroyed together with the end of the application process, it also means that you do not need to manually call applicationScope.cancel()
.
A more elegant way to manually inject is to create a ApplicationContainer
container class to hold the application-level scoped type. This helps separation of concerns, because the container class has the following responsibilities:
- Handle the logic of how to construct the exact type;
- Holds the type instance of the container-level scope;
- Returns an instance of a type that is scoped or unscoped.
class ApplicationDiContainer {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val myRepository = MyRepository(applicationScope)
}
class MyApplication : Application() {
val applicationDiContainer = ApplicationDiContainer()
}
Description: The container class always returns the same instance of the type that is scoped, and always returns a different instance of the type that is not scoped. Limiting the scope of the type to the container class is expensive . This is because the scoped object will always exist in memory before the component is destroyed, so it is only used in scenarios that really need to limit the scope.
In the ApplicationDiContainer
example above, all types are scoped. If MyRepository does not need to be scoped to Application, we can do this:
class ApplicationDiContainer {
// 限定作用域类型。永远返回相同的实例
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
// 未限定作用域类型。永远返回不同实例
fun getMyRepository(): MyRepository {
return MyRepository(applicationScope)
}
}
Use Hilt in the application
ApplicationDiContainer
(or even more) can be generated at compile time by using annotations! In addition to the Application
class, Hilt also provides a container for most of the Android Framework
Configure your application Hilt and create Application
class vessel, can Application
use class @HiltAndroidApp
comment.
@HiltAndroidApp
class MyApplication : Application()
At this point, the application DI container is ready to use. We just need to let Hilt know how to provide different types of instances.
Description : In Hilt, the container class is referenced as a component. The component associated withApplication
SingletonComponent
. Please refer to - Hilt provides a list of components :
Construction method injection
For classes where we can access the constructor, constructor injection is a simple solution to let Hilt know how to provide an instance of the type, because we only need to add the @Inject annotation to the constructor:
@Singleton // 限定作用域为 SingletonComponent
class MyRepository @Inject constructor(
private val externalScope: CoroutineScope
) {
/* ... */
}
This lets Hilt know that in order to provide an MyRepository
class, an instance of CoroutineScope
needs to be passed as a dependency. Hilt generates code at compile time to ensure that the required dependencies can be correctly created and passed in when constructing an instance of the type, or an error is reported when the conditions are insufficient. Use the @Singleton
annotation to limit the scope of the class to SingletonContainer
.
At this time, Hilt does not yet know how to provide CoroutineScope dependencies that meet the requirements, because we have not yet told Hilt what to do. The next section will show how to let Hilt know which dependencies should be passed.
Description : Hilt provides a variety of annotations to limit the scope of types to various Hilt existing components. Please refer to- Hilt provides the component list .
bind
binding is a common term in Hilt, which shows how Hilt knows how to provide an instance of the type as a dependency of information . We can say that the above code snippet uses @Inject to add bindings to Hilt.
The binding follows the component hierarchy . In SingletonComponent
available binding in ActivityComponent
are also available.
Scope undefined type binding (if above MyRepository
codes remove @Singleton
is an example), in any Hilt components are available. Limit the scope of the binding to a component, such as @Singleton
annotated by MyRepository
, which can be used in components in the current scope and components below this level.
Provide type
Through the above, we need to let Hilt know how to provide the appropriate CoroutineScope
dependencies. However, CoroutineScope
is an interface type provided by an external dependency library, so we cannot use constructor injection as we did with the MyRepository class before. Instead, the solution is to use the module let Hilt know which code to execute to provide type instances.
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton // 永远提供相同实例
@Provides
fun providesCoroutineScope(): CoroutineScope {
// 当提供 CoroutineScope 实例时,执行如下代码
return CoroutineScope(SupervisorJob() + Dispatchers.Default)
}
}
@Provides
annotation methods simultaneously @Singleton
comment, let Hilt always return the same CoroutineScope
instance. This is because any task that needs to follow the application life cycle should be created using the same instance CoroutineScope
The Hilt module annotated by @InstallIn
indicates which Hilt component the binding is loaded into (including components below the component level). In our case, SingletonComponent
requires application-level CoroutineScope
, and the binding also needs to be loaded into SingletonComponent
.
If you use Hilt's jargon, it can be said that we have added a CoroutineScope binding. At this point, Hilt knows how to provide a CoroutineScope instance.
However, the above code snippets can still be optimized. coroutine is not a good implementation of , we need to inject them make these Dispatchers configurable and easy to test . Based on the previous code, we can create a new Hilt module and let it know which Dispatcher needs to be injected for each situation: main, default or IO.
provides the implementation of
We need to provide different implementations of the CoroutineDispatcher
In other words, we need different bindings of the same type.
We can use qualifier to let Hilt know which binding or implementation needs to be used in each case. The qualifier is just a comment between you and Hilt to identify a specific binding. Let's create a qualifier for each CoroutineDispatcher
// CoroutinesQualifiers.kt 文件
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DefaultDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class IoDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MainDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainImmediateDispatcher
Next, use these qualifiers in the Hilt module to annotate different @Provides
methods to represent specific bindings. @DefaultDispatcher
qualifier annotation method returns to the default Dispatcher, and the rest of the qualifiers will not be repeated.
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesDispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
It should be noted that these CoroutineDispatchers
do not need to be scoped to SingletonComponent
. Every time these dependencies are needed, Hilt calls the method annotated by @Provides to return the corresponding CoroutineDispatcher
.
Provides CoroutineScope with application-level scope
In order to get rid of the hard-coded CoroutineDispatcher
from our previous application-level scoped CoroutineScope
code, we need to inject the default Dispatcher provided by Hilt. To this end, we can pass in the type we want to inject: CoroutineDispatcher
, and use the corresponding qualifier @DefaultDispatcher
as a dependency in the method of providing application level CoroutineScope
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton
@Provides
fun providesCoroutineScope(
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
}
Since Hilt has multiple bindings CoroutineDispatcher
CoroutineDispatcher
used as a dependency, we use the @DefaultDispatcher
annotation to disambiguate it.
Application level scope qualifier
Although we currently do not need CoroutineScope
(in the future, we may need UserCoroutineScope
), but adding qualifiers to the application level CoroutineScope
can improve its readability when injected as a dependency.
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton
@ApplicationScope
@Provides
fun providesCoroutineScope(
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
}
Since MyRepository
depends on the CoroutineScope
, it is very clear which implementation the externalScope uses:
@Singleton
class MyRepository @Inject constructor(
@ApplicationScope private val externalScope: CoroutineScope
) { /* ... */ }
Replace Dispatcher in instrumentation test
As mentioned above, we should inject the Dispatcher to make testing easier and have full control over what happens. For the instrumentation test, we hope that Espresso will wait for the end of the coroutine.
We can use AsyncTask API instead of using Espresso idle resource create a custom CoroutineDispatcher to wait for the end of the coroutine. Even though AsyncTask has been deprecated in Android API 30, Espresso will hook into its thread pool to check for idleness. Therefore, any coroutine that should be executed in the background can be executed in the thread pool of AsyncTask.
In the test, you can use Hilt TestInstallIn
API to let Hilt provide a different type of implementation. This is similar to the different Dispatcher provided above. We can androidTest
package to provide different Dispatcher implementations.
// androidTest/projectPath/TestCoroutinesDispatchersMouule.kt 文件
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [CoroutinesDispatchersModule::class]
)
@Module
object TestCoroutinesDispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher =
AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher =
AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
Through the above code, we let Hilt "forget" the CoroutinesDispatchersModule
used in the production code in the test. This module will be replaced with TestCoroutinesDispatchersModule
, which uses the thread pool of AsyncTask to handle background work, while Dispatchers.Main
acts on the main thread, which is also the target thread that Espresso is waiting for.
warning : This is actually achieved by the hack of the way, although not worth showing off, but because Espresso is currently no way of knowing CoroutineDispatcher
is in an idle state ( Issue link ), so coroutine It cannot be perfectly integrated with it. Because Espresso does not use free resources to check whether the executor is free, but by means of whether there is content in the message queue, AsyncTask.THREAD_POOL_EXECUTOR
is currently the best alternative. For these reasons, it IdlingThreadPoolExecutor , and unfortunately, when the coroutine is compiled into a state machine, the IdlingThreadPoolPool is suspended. Think that the thread pool is free.
For more information about testing, please refer to Hilt Test Guide .
Through this article, you have learned how to use Hilt to create an application-level CoroutineScope
as dependency injection, how to inject different CoroutineDispatcher
instances, and how to replace their implementation in the test.
You are welcome to click here to submit feedback to us, or share your favorite content or problems found. Your feedback is very important to us, thank you for your support!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。