Home » Android » android – Using @Component.Builder with constructor params

android – Using @Component.Builder with constructor params

Posted by: admin May 14, 2020 Leave a comment

Questions:

I’m trying to learn dagger and kotlin and mvvm in one so please forgive me if this question is odd.

If I have a NetworkModule, which basically provides retrofit to the app, I think it would be a good idea to pass in the base url for which we want to build retrofit. I can do it the old way by passing it in via the App’s component build function, but can’t figure out how to do it via the @Component.Builder method. Attempt:

App.kt

DaggerAppComponent.builder()
            .application(this)
            .networkModule(BuildConfig.BASE_URL)
            .build()
            .inject(this)

AppComponent.kt

@Singleton
@Component(modules = arrayOf(
        AppModule::class,
        NetworkModule::class,
        AndroidInjectionModule::class,
        ActivityBuilder::class))
interface AppComponent {

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun application(application: Application): Builder

        @BindsInstance
        fun networkModule(baseUrl: String): Builder

        fun build(): AppComponent

    }

    fun inject(app: App)
}

NetworkModule.kt

@Module
class NetworkModule(baseUrl: String) {

    //Attempt to force a public setter for Dagger
    var baseUrl: String = baseUrl
        set

    @Provides
    @Singleton
    fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
        val loggingInterceptor = HttpLoggingInterceptor { message -> Timber.d(message) }
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        return loggingInterceptor
    }

    @Provides
    @Singleton
    fun provideOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient {
        val httpClientBuilder = OkHttpClient.Builder()
        if (BuildConfig.DEBUG) httpClientBuilder.addInterceptor(httpLoggingInterceptor)
        return httpClientBuilder.build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()
    }
}

The error is quite clear but I still don’t understand what it means 🙂
@Component.Builder is missing setters for required modules or components: [core.sdk.di.module.NetworkModule]. I tried to force the module to create a public setter for the base url (although I think that’s not needed).

How to&Answers:

To create a component all the modules need to be supplied. Now Dagger can create any module that has a no-arg constructor itself, so that you don’t have to pass it in.

@Module
class NetworkModule(baseUrl: String) { /* ... */ }

You declare a module that has a parameter in it’s construcot, hence Dagger can’t construct it. This is what the error tries to tell you:

@Component.Builder is missing setters for required modules or components: [core.sdk.di.module.NetworkModule]

You have a module that has to be added manually (because Dagger can’t construct it) but your @Component.Builder does not have a way to include it.

What you provide is a binding for String as your base url, which is not the very best idea, since you might want to use multiple objects of String in your project. You should consider using a @Qualifier, but more on this later.

@Component.Builder
interface Builder {

  @BindsInstance
  fun application(application: Application): Builder

  // binds a `String` to the component
  @BindsInstance
  fun networkModule(baseUrl: String): Builder

  // no method to set the NetworkModule!

  fun build(): AppComponent
}

To resolve your issue you have 2 options. You can either create a setter and pass the url to the module constructor yourself, then set the module on the component (1) or you can bind the url and simply consume it in your module, removing the parameter from the constructor (2).


(1) Create the module yourself

This is the somewhat easier method since you just create the module yourself and pass it into the Dagger component builder. All you have to do is replace the binding of your String with a setter for the module instead.

@Component.Builder
interface Builder {

  @BindsInstance
  fun application(application: Application): Builder

  // add the module manually
  fun networkModule(networkModule: NetworkModule): Builder

  fun build(): AppComponent
}

When creating the component you now have to add the module to the builder.

builder
  .application(app)
  .networkModule(NetworkModule(MY_BASE_URL)) // create the module yourself
  .build()

(2) Actually bind the url

To bind the url to the component and simply use it in your module—as it seems you intended— you have to remove the parameter from your constructor.

@Module
class NetworkModule() { /* ... */ } // no-arg constructor

By doing so Dagger can now create the module and you’ll be rid of that error. Instead of putting the url in the constructor we want to get it passed in by Dagger, but as mentioned above using String is not optimal, since it literally tells Dagger that there you want that value for any String. You should use a qualifier to add a proper distinction, e.g. @Named("baseUrl") String.
More information about qualifiers

@Component.Builder
interface Builder {

  @BindsInstance
  fun application(application: Application): Builder

  // bind a `String` named "baseUrl" to this component
  @BindsInstance
  fun baseUrl(@Named("baseUrl") baseUrl: String): Builder

  fun build(): AppComponent
}

Dagger now knows that there is a @Named("baseUrl") String that we now also can use in our module.

@Module
class NetworkModule() {

  // other methods omitted

  @Provides
  @Singleton
  fun provideRetrofit(
          // request a String named baseUrl from Dagger
          @Named("baseUrl") baseUrl: String, 
          okHttpClient: OkHttpClient
  ): Retrofit {
    return Retrofit.Builder()
            .baseUrl(baseUrl)
            // ...
            .build()
  }

And that’s it.

Answer:

@Component.Builder is missing setters for required modules or components: [core.sdk.di.module.NetworkModule]

The error is very literal, the builder is missing a method to specify the exact type NetworkModule which it does not know how to build otherwise.

@BindsInstance
fun networkModule(baseUrl: String): Builder

This piece of code binds the type String, it does not have an effect on the creation of NetworkModule. Since NetworkModule cannot be injected and does not have a zero-argument constructor it must be set using the type of the builder directly:

fun networkModule(module: NetworkModule): Builder

In App.kt you then rewrite the builder with .networkModule(NetworkModule(BuildConfig.BASE_URL)) and it should construct successfully.