Issue
I'm creating a pet project with Hilt, and perhaps I'm having this issue because I'm installing everything in SingletonComponent::class, and perhaps I should create components for each one.
The pet project has a NetworkModule, UserPrefsModule, and the problem appeared when I was trying to create an Authenticator for OkHttp3.
This is my network module
@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()
}
Then my UserPrefsModule is :
@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)
}
}
Then I have an 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
)
}
The problem started to happen when I created the AccessTokenRefreshDataSourceImpl that I need the AuthenticationService and UserPrefsDataSource, and I'm getting this error :
error: [Dagger/DependencyCycle] Found a dependency cycle: public abstract static class SingletonC implements App_GeneratedInjector,
For each feature like Login, SignIn, Verification, etc.. I was creating a new @Module as this :
@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
}
Constructor of AccessTokenAutenticator
class AccessTokenAuthenticator @Inject constructor(
private val accessTokenRefreshDataSource: AccessTokenRefreshDataSource,
private val userPrefsDataSource: UserPrefsDataSource,
) : Authenticator {
Constructor of AccessTokenRefreshDatasource
class AccessTokenRefreshDataSourceImpl @Inject constructor(
private val authenticationService: AuthenticationService,
private val userPrefsDataSource: UserPrefsDataSource,
) : AccessTokenRefreshDataSource {
Note I have everything in a @Module separated by features for a future be able to modularise the app.
Solution
In most programming languages, if you require an instance of B to construct A and an instance of A to construct B, then you won't be able to construct either.
Here:
- AccessTokenRefreshDataSource requires AuthenticationService
- AuthenticationService requires Retrofit
- Retrofit requires OkHttpClient
- OkHttpClient requires Authenticator
- Authenticator requires AccessTokenRefreshDataSource
...and consequently, regardless of your module or component structure, Dagger can't create any of those instances first.
However, if your AccessTokenRefreshDataSourceImpl does not need its AuthenticationService instance within the constructor itself, you can replace it with Provider<AuthenticationService>: Dagger automatically lets you inject Provider<T> for any T in the graph, among other useful bindings. This allows Dagger to create your AccessTokenRefreshDataSource without first creating an AuthenticationService, with the promise that once the object graph is created your AccessTokenRefreshDataSource can receive the singleton AuthenticationService instance it needs. After you inject the provider, just call authenticationServiceProvider.get() to get the instance wherever you need it (presumably outside the constructor).
Of course, you can solve your problem with the same refactor anywhere else in your graph you control. AccessTokenAuthenticator is also a reasonable refactor point, assuming you've written it yourself and thus can modify its constructor.
Points discussed in the comments:
- You can always inject a
Provider<T>instead of any bindingTin your graph. In addition to being valuable for breaking dependency cycles, it can also be handy if your dependency-injected class needs to instantiate an arbitrary number of that object, or if creating the object takes a lot of memory or classloading and you want to delay it until later. Of course, if the object is cheap to construct without dependency cycles and you expect to callget()exactly once, then you can skip that and directly injectTas you've done here.Provider<T>is a single-method object. Callingget()on it is the same as calling a getter of typeTon the Component itself. If the object is unscoped, you get a new one; if the object is scoped, you get the one that Dagger has stored in the Component.Generally speaking you can just inject the Provider and call
geton it directly:class AccessTokenRefreshDataSourceImpl @Inject constructor( private val authenticationServiceProvider: Provider<AuthenticationService>, private val userPrefsDataSource: UserPrefsDataSource, ) : AccessTokenRefreshDataSource {... and then rather than using
this.authenticationService.someMethod()directly, usethis.authenticationServiceProvider.get().someMethod(). Ma3x pointed out in the comments that if you declareval authenticationService get() = authenticationServiceProvider.get()as a class field, Kotlin can abstract away the fact that there's a call toget()involved and you won't need to make any other changes to AccessTokenRefreshDataSourceImpl.You will also need to change the
@Providesmethod in your Module, but only because you're not taking full advantage of the@Injectannotation on your AccessTokenRefreshDataSourceImpl as below.@Singleton @Provides fun providesAccessTokenRefreshDataSource( userPrefsDataSource: UserPrefsDataSource, authenticationServiceProvider: Provider<AuthenticationService>, // here ): AccessTokenRefreshDataSource = AccessTokenRefreshDataSourceImpl( authenticationServiceProvider /* and here */, userPrefsDataSource )
- It is generally not necessary to use
@Providesto refer to an@Inject-annotated constructor.@Providesis useful when you can't change the constructor to make it@Inject.@Injectcan be less maintenance because then you don't need to copy@Providesmethod arguments to your constructor; Dagger will do that for you. Read more here.- If you do use
@Injectand delete your@Providesmethod, you might still want to use@Bindsto indicate that your AccessTokenRefreshDataSource should be bound to AccessTokenRefreshDataSourceImpl, though then you'll need to decide how to put@Bindsand@Providesin the same Module. In Java 8 you can do this by making your@Providesmethodsstaticand putting them on an interface, but in Kotlin it might be easier to create a nested interface and install that using@Module(includes = ...). Read more here.
- If you do use
Answered By - Jeff Bowman
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.