I have this dependency:
#Singleton
class SpiceMix #Inject constructor(#field:[Named("oregano")] private val oregano: Spice,
#field:[Named("sage")] private val sage: Spice,
#field:[Named("rosemary")] private val rosemary: Spice)
And a module to fulfill its dependencies:
#Module
class SpiceModule {
#Provides
#Named("oregano")
#Singleton
fun provideOregano(): Spice = Oregano()
#Provides
#Named("sage")
#Singleton
fun provideSage(): Spice = Sage()
#Provides
#Named("rosemary")
#Singleton
fun provideRosemary(): Spice = Rosemary()
The SpiceMix is then injected in various locations of my app.
However, this does not compile and I get an error:
Spice cannot be provided without an #Provides-annotated method
I think the #Named annotations do not quite work in my constructor signature. I am not quite sure how I can make it work.
Note: this compiles fine if I ditch the Named annotations and change the types of the constructor parameters to their concrete forms. However, Spice is an interface, and I need it for mocking purposes in my tests.
What can I do?
You want to annotate the constructor parameters if you're doing constructor injection, and not the fields - use the #param: annotation target:
#Singleton
class SpiceMix #Inject constructor(#param:Named("oregano") private val oregano: Spice,
#param:Named("sage") private val sage: Spice,
#param:Named("rosemary") private val rosemary: Spice)
Edit: actually, since the resolution order for annotation targets is
param;
property;
field.
according to the docs, having no annotation target should also annotate the parameter of the constructor. So you can just drop the target altogether:
#Singleton
class SpiceMix #Inject constructor(#Named("oregano") private val oregano: Spice,
#Named("sage") private val sage: Spice,
#Named("rosemary") private val rosemary: Spice)
Related
I have just learnt manual dependency injection, but I am trying out Hilt to handle these dependency injections.
I want to inject a ViewModel into a Fragment. The fragment is contained within an Activity. Right now, I have added the annotations to Application, Activity, and Fragment.
#HiltAndroidApp
class MovieCatalogueApplication : Application()
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
}
#AndroidEntryPoint
class HomeFragment : Fragment() {
private lateinit var binding: FragHomeBinding
private val viewmodel: HomeViewModel by viewModels()
...
As can be seen, my HomeFragment depends on HomeViewModel. I have added a ViewModel injection as described here like so.
class HomeViewModel #ViewModelInject constructor(
private val movieRepository: MovieRepository,
private val showRepository: ShowRepository,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
...
}
However, the ViewModel requires two repositories. Right now, my MovieRepository is like so.
class MovieRepository (private val movieApi: MovieService) {
...
}
In the above code, MovieService will be created by Retrofit using the Retrofit.create(class) method. The interface used to create MovieService is like so.
interface MovieService {
...
}
To get my Retrofit instance, I am using the following code.
object RetrofitService {
...
private var _retrofit: Retrofit? = null
val retrofit: Retrofit
get() {
return when (_retrofit) {
null -> {
_retrofit = Retrofit.Builder()
.client(client)
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
_retrofit!!
}
else -> _retrofit!!
}
}
}
I am not too sure how I can inject the Retrofit into the Repository to be used by my ViewModel later on. Could someone give me some pointers or step-by-step instructions on how to do this?
Apparently, it is not as hard as it seems.
You have to first define the binding information to Hilt. Binding information tells Hilt how to provide the instances of the dependency specified. Because MovieService is created using a Retrofit (which is a 3rd-party class not created by yourself) using the builder pattern, you can't use the constructor injection and you have to instead use Hilt modules and the annotation #Provides to tell Hilt about this binding information.
As described in the doc, the annotated function in the Hilt module you have created will supply the following information to Hilt so that Hilt can provide the instances of the dependency.
• The function return type tells Hilt what type the function provides instances of.
• The function parameters tell Hilt the dependencies of the corresponding type.
• The function body tells Hilt how to provide an instance of the corresponding type. Hilt executes the function body every time it needs to provide an instance of that type.
In the end, you only need to modify the MovieRepository class, add a module for each repository, and annotate the function that tells Hilt how to provide the service instance created with Retrofit with #Provides.
Code.
class MovieRepository #Inject constructor(
private val movieApi: MovieService
) {
...
}
interface MovieService {
...
}
#Module
#InstallIn(ActivityRetainedComponent::class)
object MovieModule {
#Provides
fun provideMovieService(): MovieService
= RetrofitService.retrofit.create(MovieService::class.java)
}
As you can see, the ActivityRetainedComponent is referred in the #InstallIn annotation because the Repository is to be injected to a ViewModel. Each Android component is associated to different Hilt components.
I have a simple class as below
class MainString(val msg: String)
I want to inject with different argument to it, so I use the #Named Qualifier as per the guide shown in https://google.github.io/dagger/users-guide
With that my AppModule has
#Provides #Named("Two")
fun provideTwoMainString(): MainString {
return MainString("Two")
}
#Provides #Named("One")
fun provideOneMainString(): MainString {
return MainString("One")
}
And in my MainActivity, I just call
#Inject #Named("One")
lateinit var stringOne: MainString
#Inject #Named("Two")
lateinit var stringTwo: MainString
However, when I compile, it complains
Error:(11, 1) error: com.elyeproj.demo_dagger_scope.MainString cannot be provided without an #Inject constructor or from an #Provides- or #Produces-annotated method.
It seems to want me to provide another Provider without the qualifier. So if I add the below, all will compiles. But it is not of used to me, as I want to have different argument injection.
#Provides
fun provideMainString(): MainString {
return MainString("Solo")
}
What have I done wrong?
Annotation work slightly different on kotlin. look this doc
You have to annotate the field as:
#Inject #field:Named("Two")
lateinit var stringOne: MainString
If you add the following to your qualifier annotation:
#Target(FIELD, VALUE_PARAMETER, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
then you won't have to add "field:"
For example, with:
#Qualifier
#Retention(RUNTIME)
#Target(FIELD, VALUE_PARAMETER, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
annotation class One
you can inject as follows:
#Inject #One lateinit var stringOne: String
Unfortunately #Named doesn't specify the #Target(..), so just create your own annotations. #Named is a bad idea anyway, since it's using string literals.
1) If you are using a qualifier like following, here 'OmdbService'
#Qualifier
public annotation class OmdbService
Then use
#Inject #field:OmdbService lateinit var retrofitOmdbService: Retrofit
2) If are using a named provider like following, here 'orangeservice_retrofit'
#Provides
#OrangeApplicationScope
#Named("orangeservice_retrofit")
fun retrofit(okHttpClient :OkHttpClient, gson : Gson, c :Context): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.baseUrl(c.getString(R.string.base_url))
.build()
}
Then use
#Inject #field:Named("orangeservice_retrofit") lateinit var retrofitOrangeService: Retrofit
I have the presenter
class PhonePresenter #Inject constructor(
private val preference: DataPreference,
private val ioScheduler: Scheduler = Schedulers.io())
ioScheduler is a default parameter. I want to inject only preference: DataPreference. Now I have the exception
[dagger.android.AndroidInjector.inject(T)] io.reactivex.Scheduler cannot be provided without an #Provides-annotated method.
Is there any way to define parameters which I want to inject in a constructor?
Make inject constructor with secondary constructor
class PhonePresenter(
private val preference: DataPreference,
private val ioScheduler: Scheduler) {
#Inject constructor(preference: DataPreference) : this(preference, Schedulers.io())
}
Dagger is responsible for injection, let it do it's job. Don't use default parameters (Dagger doesn't care), this will force you to make concious decisions about your dependencies.
Two approaches come to mind:
1. Use Dagger to inject
Create a qualifier so Dagger can diferentiate between types of schedulers you might want to inject and a module that provides default IO scheduler.
#Qualifier
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#MustBeDocumented
annotation class ForIo
#Module
class SchedulerModule {
#Provides #ForIo
fun provideIoScheduler() : Scheduler = Schedulers.io()
}
class PhonePresenter #Inject constructor(
private val preference: DataPreference,
#ForIo private val ioScheduler: Scheduler
) { ... }
Add SchedulerModule to your component as usual.
The correct way to supply different arguments is to use a different component with different modules specialized e.g. for testing. Or when testing you'll call the constructor manually.
2. Avoid Dagger in this case
Alternatively you can remove the IO scheduler from constructor parameters. The names suggests it's never going to be anything else than Schedulers.io() so it makes little sense to make it parameterized; make it an implementation detail instead.
class PhonePresenter #Inject constructor(private val preference: DataPreference) {
private val ioScheduler = Schedulers.io()
...
}
I have the following Module :
#Module
class HomeModule(private val context: Context) {
#Provides
fun provideContext() = context
#Provides
fun provideHomeUi(): HomeUi {
return HomeUi()
}
#Provides
#Singleton
fun provideHomePresenter(homeUi: HomeUi): HomePresenter {
return HomePresenter(homeUi)
}
}
Those injected fields in HomeUi.kt
#Inject lateinit var context: Context
#Inject lateinit var presenter: HomePresenter
And this one in HomePresenter.kt
#Inject lateinit var context: Context
Here my Deps Component
#Singleton
#Component(modules = arrayOf(
NetworkModule::class,
HomeModule::class
))
interface Deps {
fun inject(homePresenter: HomePresenter)
fun inject(homeActivity: HomeActivity)
fun inject(homeUi: HomeUi)
}
I am using Dagger 2.10 but a StackOverflowError is thrown. I am looking for a way to avoid my circular dependency.
Note : This is my HomeUi which is infinitely instantiate.
It seems like you'd be calling field injection on HomeUi from within your presenters constructor, thus triggering an infinite loop since neither object can finish being constructed without the other (?). This looks like a really bad approach and you should try to move your dependencies into the objects constructors instead of creating half-finished objects.
Use field injection primarily for objects that you can't create yourself, e.g. with Android framework types. IMHO inject(homeActivity: HomeActivity) should be the only method of your component.
Cyclic dependencies are hard to manage and there is no perfect solution, but you can try things like switching to Provider<HomePresenter> to delay the dependency and be able to resolve it this way.
The following should do what you intended, and please note how I'm using constructor injection instead of having 2 additional methods in the module.
#Singleton
#Component(modules = arrayOf(
NetworkModule::class,
HomeModule::class
))
interface Deps {
fun inject(homeActivity: HomeActivity)
}
#Module
class HomeModule(private val context: Context) {
#Provides
fun provideContext() = context
}
#Singleton
class HomeUi #Inject constructor(presenter : Provider<HomePresenter>, context : Context)
{
// use with presenter.get()
}
#Singleton
class HomePresenter #Inject constructor(homeUi : HomeUi)
Please note that using a Provider<T> is the cheapest way to resolve a cyclic dependency that I know of, but it might not be suited for every situation.
I never used Dagger 2 with kotlin but I use it in java. I usually create my Module with my view as param and my method provideHomeUi() return my view as parameter. With this you shouldnt have a StackOverflowError.
Btw, why are you using Dagger 2 with Kotlin and not a directly library for DI on Kotlin, such as Kodein, Koin etc...
Good luck.
I do have two custom annotations
#Qualifier
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class IOScheduler
and
#Qualifier
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class UIScheduler
I'm using them in my module
#Module
class UiModule {
#Provides
#UIScheduler
fun provideUiScheduler(): Scheduler = AndroidSchedulers.mainThread()
#Provides
#IOScheduler
fun provideIOScheduler(): Scheduler = Schedulers.io()
}
Of course module is added to my component, and when i'm trying to inject them it says
rx.Scheduler cannot be provided without an #Provides- or
#Produces-annotated method.
injecting
class MainPresenter(val view: MainContract.View) : MainContract.Presenter {
#Inject lateinit var api: ApiServices
#Inject lateinit var databaseManager: DatabaseManager
#Inject #UIScheduler lateinit var observeScheduler: Scheduler
#Inject #IOScheduler lateinit var subscribeScheduler: Scheduler
...
}
Funny think is that i'm using almost same think with injecting Gson for my ApiClient and it works perfectly
#Module
class ApiModule {
...
#Provides
#Singleton
#DefaultGson
fun providesGson(): Gson = GsonBuilder().create()
#Provides
#Singleton
#FormatDateGson
fun providesGsonFormatTimestamp(): Gson = GsonBuilder().setDateFormat(Constants.TIME_STAMP_FORMAT).create()
}
#Singleton
class ApiClient #Inject constructor(#DefaultGson val gson: Gson, val sharedPreferences: SharedPrefsUtils) {
...
}
In additional i can say that i do have same modules / providers structure in my java projects and it works just fine.
Use custom annotations with field: target e.g.
#Inject #field:IOScheduler lateinit var subscribeScheduler: Scheduler
In other news: Dagger refuses to do the following:
#Inject internal var something: Boolean = false // or
#Inject protected var toolbar: Toolbar? = null
The error looks like this:
Dagger does not support injection into private fields
This affects any variables without the lateinit modifier.
You'll have to expose primitives in your component and inject them manually.
Just specify the set: target so injection is done via public/internal/protected setter instead of the private backing field like this:
#set:Inject internal var something: Boolean = false // or
#set:Inject protected var toolbar: Toolbar? = null
Custom annotations
Declare your custom annotations with
#Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
-- pretty much anything except PROPERTY because that is invisible to Java. The compiler will now show an error if you try to use an annotation without get:, set: or field: target. And you need that target so the annotation takes effect.