Hilt 循环依赖

Hilt circular dependency

我正在使用 Hilt 创建一个宠物项目,也许我遇到这个问题是因为我正在 安装 SingletonComponent::class 中的所有内容,也许我应该为每一个创建组件。

宠物项目有一个 NetworkModuleUserPrefsModule,当我试图为 OkHttp3 创建一个 Authenticator 时出现了问题。

这是我的网络模块

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Singleton
    @Provides
    fun providesHttpLoggingInterceptor() = HttpLoggingInterceptor()
        .apply {
            if (BuildConfig.DEBUG) level = HttpLoggingInterceptor.Level.BODY
        }

    @Singleton
    @Provides
    fun providesErrorInterceptor(): Interceptor {
        return ErrorInterceptor()
    }

    @Singleton
    @Provides
    fun providesAccessTokenAuthenticator(
        accessTokenRefreshDataSource: AccessTokenRefreshDataSource,
        userPrefsDataSource: UserPrefsDataSource,
    ): Authenticator = AccessTokenAuthenticator(
        accessTokenRefreshDataSource,
        userPrefsDataSource,
    )

    @Singleton
    @Provides
    fun providesOkHttpClient(
        httpLoggingInterceptor: HttpLoggingInterceptor,
        errorInterceptor: ErrorInterceptor,
        authenticator: Authenticator,
    ): OkHttpClient =
        OkHttpClient
            .Builder()
            .authenticator(authenticator)
            .addInterceptor(httpLoggingInterceptor)
            .addInterceptor(errorInterceptor)
            .build()

}

那么我的UserPrefsModule是:

@Module
@InstallIn(SingletonComponent::class)
object UserPrefsModule {

    @Singleton
    @Provides
    fun provideSharedPreference(@ApplicationContext context: Context): SharedPreferences {
        return context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
    }

    @Singleton
    @Provides
    fun provideUserPrefsDataSource(sharedPreferences: SharedPreferences): UserPrefsDataSource {
        return UserPrefsDataSourceImpl(sharedPreferences)
    }
}

然后我有一个AuthenticatorModule

@Module
@InstallIn(SingletonComponent::class)
object AuthenticationModule {

    private const val BASE_URL = "http://10.0.2.2:8080/"

    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
        .addConverterFactory(MoshiConverterFactory.create())
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .build()

    @Singleton
    @Provides
    fun provideApiService(retrofit: Retrofit): AuthenticationService =
        retrofit.create(AuthenticationService::class.java)


    @Singleton
    @Provides
    fun providesAccessTokenRefreshDataSource(
        userPrefsDataSource: UserPrefsDataSource,
        authenticationService: AuthenticationService,
    ): AccessTokenRefreshDataSource = AccessTokenRefreshDataSourceImpl(
        authenticationService, userPrefsDataSource
    )
}

当我创建需要 AuthenticationServiceUserPrefsDataSourceAccessTokenRefreshDataSourceImpl 时,问题开始出现,我收到此错误:

error: [Dagger/DependencyCycle] Found a dependency cycle: public abstract static class SingletonC implements App_GeneratedInjector,

对于登录、登录、验证等每个功能我正在创建一个新的@Module,如下所示:

@Module
@InstallIn(SingletonComponent::class)
interface SignInModule {

    @Binds
    fun bindIsValidPasswordUseCase(
        isValidPasswordUseCaseImpl: IsValidPasswordUseCaseImpl,
    ): IsValidPasswordUseCase

    @Binds
    fun bindIsValidEmailUseCase(
        isValidEmailUseCase: IsValidEmailUseCaseImpl,
    ): IsValidEmailUseCase

     //Here in that Datasource I'm using the AuthenticationService from AuthenticationModule and it works
    @Binds
    fun bindSignInDataSource(
        signInDataSourceImpl: SignInDataSourceImpl
    ): SignInDataSource
}

AccessTokenAutenticator

的构造函数
class AccessTokenAuthenticator @Inject constructor(
    private val accessTokenRefreshDataSource: AccessTokenRefreshDataSource,
    private val userPrefsDataSource: UserPrefsDataSource,
) : Authenticator {

AccessTokenRefreshDatasource

的构造函数
class AccessTokenRefreshDataSourceImpl @Inject constructor(
    private val authenticationService: AuthenticationService,
    private val userPrefsDataSource: UserPrefsDataSource,
) : AccessTokenRefreshDataSource {

请注意,我将所有内容都放在 @Module 中,按功能分隔,以便将来能够模块化应用程序。

在大多数编程语言中,如果您需要一个 B 的实例来构造 A 并需要一个 A 的实例来构造 B,那么您将无法构造任何一个。

这里:

  • AccessTokenRefreshDataSource 需要 AuthenticationService
  • AuthenticationService 需要改造
  • Retrofit 需要 OkHttpClient
  • OkHttpClient 需要 Authenticator
  • 身份验证器需要 AccessTokenRefreshDataSource

...因此,无论您的模块或组件结构如何,Dagger 都无法首先创建任何这些实例

但是,如果您的 AccessTokenRefreshDataSourceImpl 在构造函数本身中不需要其 AuthenticationService 实例,您可以将其替换为 Provider<AuthenticationService>:Dagger 自动允许您为图中的任何 T 注入 Provider<T>among other useful bindings。这允许 Dagger 创建您的 AccessTokenRefreshDataSource 而无需首先创建 AuthenticationService,并承诺一旦创建了对象图,您的 AccessTokenRefreshDataSource 就可以接收它需要的单例 AuthenticationService 实例。注入提供者后,只需调用 authenticationServiceProvider.get() 即可在任何需要的地方获取实例(大概在构造函数之外)。

当然,您可以在您控制的图形中的其他任何地方使用相同的重构来解决您的问题。 AccessTokenAuthenticator 也是一个合理的重构点,假设你自己写了它,因此可以修改它的构造函数。


评论中讨论的要点:

  • 您可以始终在图表中注入 Provider<T> 而不是任何绑定 T。除了对于打破依赖循环很有价值之外,如果你的依赖注入 class 需要实例化任意数量的对象,或者如果创建对象需要大量内存或 classloading 并且您想将其延迟到以后。当然,如果在没有依赖循环的情况下构建对象的成本很低,并且您希望只调用一次 get(),那么您可以跳过它并直接注入 T,就像您在此处所做的那样。
    • Provider<T> 是单一方法对象。在其上调用 get() 与在组件本身上调用 T 类型的 getter 相同。如果对象没有作用域,你会得到一个新的;如果对象是有范围的,你会得到 Dagger 存储在组件中的对象。

    • 一般来说,你可以直接注入 Provider 并直接调用 get:

      class AccessTokenRefreshDataSourceImpl @Inject constructor(
        private val authenticationServiceProvider:
          Provider<AuthenticationService>,
        private val userPrefsDataSource: UserPrefsDataSource,
      ) : AccessTokenRefreshDataSource {
      

      ... 然后不要直接使用 this.authenticationService.someMethod(),而是使用 this.authenticationServiceProvider.get().someMethod()。 Ma3x 在评论中指出,如果您将 val authenticationService get() = authenticationServiceProvider.get() 声明为 class 字段,Kotlin 可以抽象出涉及 get() 的调用这一事实,您无需进行任何操作对 AccessTokenRefreshDataSourceImpl 的其他更改。

    • 您还需要更改模块中的 @Provides 方法,但这只是因为您没有充分利用 AccessTokenRefreshDataSourceImpl 上的 @Inject 注释,如下所示.

      @Singleton
      @Provides
      fun providesAccessTokenRefreshDataSource(
          userPrefsDataSource: UserPrefsDataSource,
          authenticationServiceProvider: Provider<AuthenticationService>,  // here
      ): AccessTokenRefreshDataSource = AccessTokenRefreshDataSourceImpl(
          authenticationServiceProvider /* and here */, userPrefsDataSource
      )
      
  • 通常不需要使用@Provides 来引用@Inject-注释的构造函数。 @Provides 在无法更改构造函数以使其成为 @Inject 时很有用。 @Inject 可以减少维护,因为这样您就不需要将 @Provides 方法参数复制到构造函数; Dagger 会为你做到这一点。
    • 如果您确实使用了 @Inject 并删除了您的 @Provides 方法,您可能仍想使用 @Binds 来指示您的 AccessTokenRefreshDataSource 应该绑定到 AccessTokenRefreshDataSourceImpl,尽管这样您您需要决定如何将 @Binds@Provides 放在同一个模块中。在 Java 8 中,您可以通过使 @Provides 方法 static 并将它们放在接口上来实现此目的,但在 Kotlin 中,创建嵌套接口并使用 [= 安装它可能更容易42=]。