Dagger: Suggestion needed for using Hilt in library modules

Created on 17 Jul 2020  路  12Comments  路  Source: google/dagger

In my sample app I have created a separate library module called core. In the core module I am using network and db related dependencies like OkHttp, Retrofit and Room. Hence to access these instances as singleton, I declared them in the hilt module.

import com.mobile.app.core.BuildConfig
import com.mobile.app.core.remote.RemoteService
import com.mobile.app.core.remote.RemoteServiceInterceptor
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton

@Module
@InstallIn(ApplicationComponent::class)
object CoreModule {

    @Provides
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient
            .Builder()
            .addInterceptor(RemoteServiceInterceptor())
            .addInterceptor(HttpLoggingInterceptor().apply {
                level =
                    if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
                    else
                        HttpLoggingInterceptor.Level.NONE
            }).build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit =
        Retrofit.Builder()
            .baseUrl("https://jsonplaceholder.typicode.com")
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    @Provides
    @Singleton
    fun provideRemoteService(retrofit: Retrofit): RemoteService =
        retrofit.create(RemoteService::class.java)
}

In my app module I have my application class as mentioned below:

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()
    }
}

My app build.gradle also implements core module:

dependencies {
implementation project(path: ':core')
}

But when I build my project, it throws error as:

Task :app:kaptDebugKotlin
error: cannot access OkHttpClient
class file for okhttp3.OkHttpClient not found
Consult the following stack trace for details.

cannot access OkHttpClient

I can overcome this error only if I change implementation to api for OkHttp client dependency which is delcared in the core module build.gradle. Is there anything I'm missing here.

Want I am trying to achieve is to have separate library modules for network service, local storage and firebase. So that I can implement this to app module as needed. Hence I started declaring Hilt module in each of these library modules like NetworkModule, StorageModule, FirebaseModule, etc. But I couldn't figure this out.

build hilt

Most helpful comment

It seems to me like Hilt is breaking everyone's ability to take advantage of gradle compile avoidance, and is polluting gradle modules with transitive dependencies.

If all of my module dependencies have to be declared api, and I can no longer have an @Module in my libraries without it being required to have @InstallIn added, what is even the point of having separated gradle modules when using hilt?

All 12 comments

Using api is correct here. This is related to https://github.com/google/dagger/issues/970

In essence, because Hilt aggregates your modules into your root app Gradle project where Dagger generates the component implementation and because such code generated references all of your binding types in other modules, then the root app Gradle project must have visibility in its classpath to all of those other Gradle modules where you define Dagger modules along with its dependencies if they are used in those Dagger modules. It's a bit hard to see, but the factories Dagger generates for your bindings end up being public APIs used downstream so restricting those at the Gradle module boundaries using implementation is not quite right.

There is no current workaround in Hilt that I know off. We are aware of the implications this causes, such as leaking classes into other Gradle modules and possibly build performance impact with regards to compile avoidance. We've have some ideas on how to fix this and we hope to get them in before declaring Hilt stable.

Thanks @danysantiago

The workaround in the issue mentioned by Dany was working the last time I checked it. Someone is mentioning it is not working with AGP 4 but I think I tried it even with AGP 4.1 with no problems.

I had faced the same problem with my project. Instead of changing implementation to api (Not Recommended), you can add dependencies of missing libraries in app's build.gradle and rebuild the project. In your case, you can add Retrofit and OkHttp dependency in app's build.gradle.

@karthikeyan1241997 May be. But I am not sure why app module should be aware of these dependencies(Retrofit+Okhttp) when the are in separate library module in my case.

Is this fixed now considering https://github.com/google/dagger/issues/970 is closed

Unfortunately no, the general issue still exists (but as @danysantiago mentions in https://github.com/google/dagger/issues/1991#issuecomment-661245887 we're still looking into it).

The issue in #970 was a very specific case of this problem where api was only needed to check validation of @Component.dependencies for cycles. For that specific case we decided to add a flag that disables that validation so that implementation would work (with the understanding that the flag should probably only be disabled in dev).

It seems to me like Hilt is breaking everyone's ability to take advantage of gradle compile avoidance, and is polluting gradle modules with transitive dependencies.

If all of my module dependencies have to be declared api, and I can no longer have an @Module in my libraries without it being required to have @InstallIn added, what is even the point of having separated gradle modules when using hilt?

There are still great benefits to making separate gradle modules, for one you can have smaller tests on them, including 'small test APKs' where the dependencies set and the modules in the classpath are reduced.

AFAIK compile avoidance is not completely lost when using api vs implementation. It's also important to clarify that this is an issue present not only in Hilt but in Dagger in general. The difference in Hilt is that it can be easier to encounter since modules are aggregated into the components whereas in vanilla Dagger you would have to specify the Dagger module class in the @Component-annotated interface, which would ultimately require you to either expose those Gradle modules via api dependencies or make the app module (or wherever the component is) depend directly on the Gradle modules containing the Dagger modules.

There are workarounds by introducing indirections in the bindings that Dagger can provide, at the cost of an increased complexity in the Gradle modules setup, one example of this is explained and shown by the following talk in Droidcon: Android at Scale @Square

It seems to me like Hilt is breaking everyone's ability to take advantage of gradle compile avoidance, and is polluting gradle modules with transitive dependencies.

One way to mitigate this problem is to have your application gradle module be tiny. The application gradle module only contains your Application class and defines the root of your dependency injection graph.

You then compose your application by pulling in the necessary dependencies. If you have build variants, this approach makes it easy to have different features in different variants.

I find that the following organization works very well:

  • app (just MyApplication.kt)
  • feature1/

    • api/

    • module/

    • impl/

  • feature2/

    • api/

    • module/

    • impl/

app/build.gradle looks like this:

  • implementation project(feature1/module)
  • implementation project(feature2/module)

feature1/module/build.gradle looks like this:

  • api project(feature1/api)
  • api project(feature1/impl)

With this approach we never run into class file not found errors.

Hi @snepalnetflix do you have any sample repository implementing the structure you have mentioned above. It would be great if you could share one. Thank you.

@kalaiselvan369 I'm working on a Hilt Android example. It's not working yet but you can get an idea of the structure. See:
https://github.com/snepalnetflix/hilt-giphy

In this project all feature/module projects declare dependencies on their api and impl using:

  • api project(":feature:api")
  • api project(":feature:impl")

I thought that this would be enough to prevent class not found errors. Unfortunately I ran into one class not found error:

error: cannot access PagingDataAdapter
  class file for androidx.paging.PagingDataAdapter not found

I was surprised by this since I haven't really run into errors like this on my real project. I think this is an infrequent enough problem that explicitly adding the paging library as a dependency on the app module is acceptable.

Was this page helpful?
0 / 5 - 0 ratings