头图

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 with Application 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!


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。