在遵循 协程最佳实际 时,您可能须要在某些类中注入利用级别作用域的 CoroutineScope,以便能够创立与利用生命周期雷同的新协程,或创立在调用者作用域之外仍能够工作的新协程。
通过本文,您将学习如何通过 Hilt 创立利用级别作用域的 CoroutineScope
,以及如何将其作为依赖项进行注入。咱们将在示例中展现如何注入不同的 CoroutineDispatcher
以及在测试中替换其实现,进一步优化协程的应用。
手动依赖项注入
在不应用任何库的状况下,遵循依赖项注入 (DI) 的最佳实际计划来 手动 创立一个利用级别作用域 的 CoroutineScope
,通常会在 Application 类中增加一个 CoroutineScope
实例变量。当创立其余对象时,手动将雷同的 CoroutineScope
实例散发到这些对象中。
class MyRepository(private val externalScope: CoroutineScope) {/* ... */}
class MyApplication : Application() {
// 利用中任何类都能够通过 applicationContext 拜访利用级别作用域的类型
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val myRepository = MyRepository(applicationScope)
}
因为在 Android 中没有牢靠的办法来获取 Application
销毁的机会,并且利用级别的作用域以及任何正在执行的工作都将同利用过程的完结一起销毁,也意味着您无需手动调用 applicationScope.cancel()
。
手动注入更优雅的做法是创立一个 ApplicationContainer
容器类来持有利用级别作用域的类型。这有助于关注点拆散,因为容器类具备如下职责:
- 解决如何结构确切类型的逻辑;
- 持有容器级别作用域的类型实例;
- 返回限定作用域或未限定作用域的类型实例。
class ApplicationDiContainer {val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val myRepository = MyRepository(applicationScope)
}
class MyApplication : Application() {val applicationDiContainer = ApplicationDiContainer()
}
阐明: 容器类永远返回被限定作用域的类型的雷同实例,并且永远返回未被限定作用域的类型的不同实例。将类型的作用域限定到容器类中 老本很高,这是因为在组件销毁之前,被限定作用域的对象将始终存在于内存中,所以仅在真正须要限定作用域的场景应用。
在上述 ApplicationDiContainer
示例中,所有的类型都被限定了作用域。如果 MyRepository 无需将作用域限定为 Application,咱们能够这样做:
class ApplicationDiContainer {
// 限定作用域类型。永远返回雷同的实例
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
// 未限定作用域类型。永远返回不同实例
fun getMyRepository(): MyRepository {return MyRepository(applicationScope)
}
}
在利用中应用 Hilt
在 Hilt 中,能够通过应用注解在编译期生成 ApplicationDiContainer
的内容 (甚至更多)!并且 Hilt 除 Application
类外,还为大部分 Android Framework 类提供了容器。
在您的利用中配置 Hilt 并且创立 Application
类的容器,能够在 Application
类中应用 @HiltAndroidApp
注解。
@HiltAndroidApp
class MyApplication : Application()
此时,利用 DI 容器曾经能够应用了。咱们只须要让 Hilt 晓得如何提供不同类型的实例。
阐明 : 在 Hilt 中,容器类被援用为组件。与
Application
关联的组件被称为SingletonComponent
。请参阅 —— Hilt 提供的组件列表:
构造方法注入
对于咱们能够拜访构造方法的类,构造方法注入是一个简略的计划来让 Hilt 晓得如何提供类型的实例,因为咱们只须要在结构器上减少 @Inject 注解:
@Singleton // 限定作用域为 SingletonComponent
class MyRepository @Inject constructor(private val externalScope: CoroutineScope) {/* ... */}
这让 Hilt 晓得,为了提供一个 MyRepository
类的实例,须要传递一个 CoroutineScope
的实例作为依赖项。Hilt 在编译期生成代码,以确保构造类型的实例时能够正确创立并传入所需依赖项,或者在条件有余时报错。应用 @Singleton
注解,将该类的作用域限定为 SingletonContainer
。
此时,Hilt 还不晓得如何提供满足要求的 CoroutineScope 依赖项,因为咱们还没有通知 Hilt 该如何解决。接下来的局部将展现如何让 Hilt 晓得应该传递哪些依赖项。
阐明 : Hilt 提供了多种注解,来实现将类型的作用域限定到各种 Hilt 的现有组件中。请参阅 —— Hilt 提供的组件列表。
绑定
绑定 是 Hilt 中的一个常见术语,它表明了 Hilt 所知的如何提供类型的实例作为依赖项的 信息。咱们能够说,上文的代码片段就是应用 @Inject 在 Hilt 中增加了绑定。
绑定遵循 组件层次结构。在 SingletonComponent
中可用的绑定,在 ActivityComponent
中同样可用。
未限定作用域的类型的绑定 (如果上文的 MyRepository
代码去掉 @Singleton
就是一个例子),在 任何 Hilt 组件中都可用。将绑定的作用域限定到一个组件,例如被 @Singleton
注解的 MyRepository
,能够在以后作用域的组件以及该层级以下的组件中应用。
通过模块提供类型
通过上述内容,咱们须要让 Hilt 晓得如何提供适合的 CoroutineScope
的依赖项。然而 CoroutineScope
是一个内部依赖库提供的接口类型,所以咱们不能像之前解决 MyRepository 类一样应用构造方法注入。取而代之的计划是通过 应用模块,让 Hilt 晓得执行哪些代码来提供类型实例。
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton // 永远提供雷同实例
@Provides
fun providesCoroutineScope(): CoroutineScope {
// 当提供 CoroutineScope 实例时,执行如下代码
return CoroutineScope(SupervisorJob() + Dispatchers.Default)
}
}
@Provides
注解的办法同时被 @Singleton
注解,让 Hilt 总是 返回雷同的 CoroutineScope
实例。这是因为任何须要遵循利用生命周期的工作都应该应用遵循利用生命周期的 CoroutineScope
的同一实例创立。
被 @InstallIn
注解的 Hilt 模块,表明该绑定被装载到哪个 Hilt 组件中 (蕴含该组件层级以下的组件)。在咱们的案例中,被限定作用域到 SingletonComponent
上的 MyRepository,须要利用级别的 CoroutineScope
,该绑定同样须要被装载到 SingletonComponent
中。
如果应用 Hilt 的行话,能够说成咱们增加了一个 CoroutineScope 绑定,至此,Hilt 就晓得如何提供 CoroutineScope 实例了。
然而,上述代码片段仍能够优化。协程中硬编码 Dispatcher 不是良好的实现,咱们须要注入它们 使得这些 Dispatcher 可配置并且易于测试。基于之前的代码,咱们能够创立一个新的 Hilt 模块,让它晓得为每种状况须要注入哪个 Dispatcher: main、default 还是 IO。
提供 CoroutineDispatcher 的实现
咱们须要提供雷同类型 CoroutineDispatcher
的不同实现。换句话说就是,咱们须要雷同类型的不同绑定。
咱们能够应用 限定符 来让 Hilt 晓得每种状况须要应用哪种绑定或者实现。限定符只是您和 Hilt 之间用来标识特定绑定的注解。让咱们为每一种 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
接下来,在 Hilt 模块中应用这些限定符注解不同的 @Provides
办法来示意特定的绑定。@DefaultDispatcher
限定符注解的办法返回默认的 Dispatcher,其余限定符不再赘述。
@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}
须要留神,这些 CoroutineDispatchers
无需限定作用域到 SingletonComponent
。每次须要这些依赖项时,Hilt 调用被 @Provides 注解的办法返回对应的 CoroutineDispatcher
。
提供利用级别作用域的 CoroutineScope
为了从咱们之前的利用级别作用域的 CoroutineScope
代码中解脱硬编码 CoroutineDispatcher
,咱们须要注入 Hilt 提供的默认 Dispatcher。为此,咱们能够传入咱们想要注入的类型: CoroutineDispatcher
,在提供利用级别 CoroutineScope
的办法中应用对应的限定符 @DefaultDispatcher
作为依赖项。
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton
@Provides
fun providesCoroutineScope(@DefaultDispatcher defaultDispatcher: CoroutineDispatcher): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
}
因为 Hilt 对 CoroutineDispatcher
类型具备多个绑定,因而当 CoroutineDispatcher
用作依赖项时,咱们应用 @DefaultDispatcher
注解打消它的歧义。
利用级别作用域限定符
尽管咱们目前不须要 CoroutineScope
的多个绑定 (将来咱们可能须要像 UserCoroutineScope
这样的协程作用域),然而向利用级别 CoroutineScope
增加限定符能够进步其作为依赖项注入时的可读性。
@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)
}
因为 MyRepository
依赖该 CoroutineScope
,因此能够十分清晰地晓得 externalScope 应用哪种实现:
@Singleton
class MyRepository @Inject constructor(@ApplicationScope private val externalScope: CoroutineScope) {/* ... */}
在插桩测试中替换 Dispatcher
如上所述,咱们应该注入 Dispatcher 使测试更容易并能够齐全管制产生的事件。对于插桩测试,咱们心愿 Espresso 期待协程完结。
咱们能够利用 AsyncTask API 来代替应用 Espresso 闲暇资源 创立自定义 CoroutineDispatcher,来期待协程的完结。即便 AsyncTask 曾经在 Android API 30 中被弃用,但 Espresso 会 hook 到其线程池中来查看闲暇状况。因而,任何应该在后盾执行的协程都能够在 AsyncTask 的线程池中执行。
在测试中能够应用 Hilt TestInstallIn
API 让 Hilt 提供一个类型的不同实现。这与上文提供不同 Dispatcher 相似,咱们能够在 androidTest
包下创立一个新文件,来提供不同的 Dispatcher 实现。
// 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}
通过上述代码,咱们让 Hilt 在测试中 “ 遗记 ” 了在生产代码中应用的 CoroutinesDispatchersModule
。该模块将会被替换为 TestCoroutinesDispatchersModule
,它应用 AsyncTask 的线程池来解决后盾工作,而 Dispatchers.Main
则作用于主线程,这也是 Espresso 期待的指标线程。
正告 : 这其实是通过 hack 的形式实现的,尽管不值得夸耀,然而因为 Espresso 目前没有方法晓得 CoroutineDispatcher
是否处于闲暇状态 (issue 链接 ),所以协程并不能与其完满的集成。因为 Espresso 不是应用闲暇资源来查看该 executor 是否闲暇,而是通过音讯队列中是否有内容的形式,所以 AsyncTask.THREAD_POOL_EXECUTOR
是目前最佳的代替计划。也正是这些起因,使得它绝对于诸如 IdlingThreadPoolExecutor 之类来说是一个更优解,并且十分可怜的是,当因为协程被编译成状态机而被挂起时,IdlingThreadPoolExecutor 会认为线程池是闲暇的。
更多对于测试的信息,请参阅 Hilt 测试指南。
通过本文,您曾经理解到如何应用 Hilt 创立一个利用级别的 CoroutineScope
作为依赖项注入,如何注入不同的 CoroutineDispatcher
实例,以及如何在测试中替换它们的实现。
欢迎您 点击这里 向咱们提交反馈,或分享您喜爱的内容、发现的问题。您的反馈对咱们十分重要,感谢您的反对!