Android - Dagger injection at runtime - android

i need to inject the class at runtime using dagger. what my problem is am getting compile time error while injecting the class locally in method and also i was not able to inject at runtime without using constant for #Named
Example
interface PerformActionInterface{
fun performAction()
}
class P1 : PerformActionInterface{
override fun performAction(){
}
}
class P2 : PerformActionInterface{
override fun performAction(){
}
}
class PerformAction #Inject constructor(){
fun perform(name : String){
#Inject
#Named(name)
performActionInterface : PerformActionInterface
performActionInterface.performAction()
}
}
were as in dagger implementation i will be doing like this
#Binds
#Named("p1")
abstract bindP1Class(p1 : P1) :PerformActionInterface
#Binds
#Named("p2")
abstract bindP1Class(p2 : P2) :PerformActionInterface
Any help for how to inject this at runtime ?

You can't annotate something in runtime, the element value in Java annotation has to be a constant expression.
But this use case can be solved by map multibind.
In your Module, in addition to just #Bind or #Provide, also annotate the abstract fun with #IntoMap and the map key (sorry for any error in my Kotlin)
#Binds
#IntoMap
#StringKey("p1")
abstract fun bindP1Class(p1: P1): PerformActionInterface
#Binds
#IntoMap
#StringKey("p2")
abstract fun bindP2Class(p2: P2): PerformActionInterface
Then in your PerformAction class, declare a dependency of a map from String to PerformActionInterface, and do whatever with the map:
// map value type can be Lazy<> or Provider<> as needed
class PerformAction #Inject constructor(
val map: Map<String, #JvmSuppressWildcards PerformActionInterface>) {
fun perform(name: String) {
map.get(name)?.performAction()
// or do something if the get returns null
}
}

Related

Can Hilt be used on Android with by viewModels to initialize an abstract viewModel field?

I'm trying to wrap my head around Hilt and the way it deals with ViewModels.
I would like my fragments to depend on abstract view models, so I can easily mock them during UI tests. Ex:
#AndroidEntryPoint
class MainFragment : Fragment() {
private val vm : AbsViewModel by viewModels()
/*
...
*/
}
#HiltViewModel
class MainViewModel(private val dependency: DependencyInterface) : AbsViewModel()
abstract class AbsViewModel : ViewModel()
Is there a way to configure by viewModels() so that it can map concrete implementations to abstract view models? Or pass a custom factory producer to viewModels() that can map concrete view models instances to abstract classes?
The exact question is also available here, but it is quite old considering hilt was still in alpha then: https://github.com/google/dagger/issues/1972
However, the solution provided there is not very desirable since it uses a string that points to the path of the concrete view model. I think this will not survive obfuscation or moving files and it can quickly become a nightmare to maintain. The answer also suggests injecting a concrete view model into the fragment during tests with all the view model's dependencies mocked, thus gaining the ability to control what happens in the test. This automatically makes my UI test depend on the implementation of said view model, which I would very much like to avoid.
Not being able to use abstract view models in my fragments makes me think I'm breaking the D in SOLID principles, which is something that I would also like to avoid.
Not the cleanest solution, but here's what I managed to do.
First create a ViewModelClassesMapper to help map an abstract class to a concrete one. I'm using a custom AbsViewModel in my case, but this can be swapped out for the regular ViewModel. Then create a custom view model provider that depends on the above mapper.
class VMClassMapper #Inject constructor (private val vmClassesMap: MutableMap<Class<out AbsViewModel>, Provider<KClass<out AbsViewModel>>>) : VMClassMapperInterface {
#Suppress("TYPE_INFERENCE_ONLY_INPUT_TYPES_WARNING")
override fun getConcreteVMClass(vmClass: Class<out AbsViewModel>): KClass<out AbsViewModel> {
return vmClassesMap[vmClass]?.get() ?: throw Exception("Concrete implementation for ${vmClass.canonicalName} not found! Provide one by using the #ViewModelKey")
}
}
interface VMClassMapperInterface {
fun getConcreteVMClass(vmClass: Class<out AbsViewModel>) : KClass<out AbsViewModel>
}
interface VMDependant<VM : AbsViewModel> : ViewModelStoreOwner {
fun getVMClass() : KClass<VM>
}
class VMProvider #Inject constructor(private val vmMapper: VMClassMapperInterface) : VMProviderInterface {
#Suppress("UNCHECKED_CAST")
override fun <VM : AbsViewModel> provideVM(dependant: VMDependant<VM>): VM {
val concreteClass = vmMapper.getConcreteVMClass(dependant.getVMClass().java)
return ViewModelProvider(dependant).get(concreteClass.java) as VM
}
}
interface VMProviderInterface {
fun <VM :AbsViewModel> provideVM(dependant: VMDependant<VM>) : VM
}
#Module
#InstallIn(SingletonComponent::class)
abstract class ViewModelProviderModule {
#Binds
abstract fun bindViewModelClassesMapper(mapper: VMClassMapper) : VMClassMapperInterface
#Binds
#Singleton
abstract fun bindVMProvider(provider: VMProvider) : VMProviderInterface
}
Then, map your concrete classes using the custom ViewModelKey annotation.
#Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class ViewModelKey(val value: KClass<out AbsViewModel>)
#Module
#InstallIn(SingletonComponent::class)
abstract class ViewModelsDI {
companion object {
#Provides
#IntoMap
#ViewModelKey(MainContracts.VM::class)
fun provideConcreteClassForMainVM() : KClass<out AbsViewModel> = MainViewModel::class
#Provides
#IntoMap
#ViewModelKey(SecondContracts.VM::class)
fun provideConcreteClassForSecondVM() : KClass<out AbsViewModel> = SecondViewModel::class
}
}
interface MainContracts {
abstract class VM : AbsViewModel() {
abstract val textLiveData : LiveData<String>
abstract fun onUpdateTextClicked()
abstract fun onPerformActionClicked()
}
}
interface SecondContracts {
abstract class VM : AbsViewModel()
}
Finally, your fragment using the abstract view model looks like this:
#AndroidEntryPoint
class MainFragment : Fragment(), VMDependant<MainContracts.VM> {
#Inject lateinit var vmProvider: VMProviderInterface
protected lateinit var vm : MainContracts.VM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vm = vmProvider.provideVM(this)
}
override fun getVMClass(): KClass<MainContracts.VM> = MainContracts.VM::class
}
It's a long way to go, but after you have the initial setup is completed, all you need to do for individual fragments is to make them implement VMDependant and provide a concrete class for YourAbsViewModel in Hilt using the #ViewModelKey.
In tests, vmProvider can then be easily mocked and forced to do your bidding.

Dagger 2 MissingBinding when swapping concretion for interface

I have two classes that I'm able to have Dagger find and inject for me to use successfully:
TrackEvent
class TrackEvent #Inject constructor(
private val getTrackingProperties: SomeClass
) : UseCase<Boolean, TrackingEvent> {
override suspend operator fun invoke(params: TrackingEvent): Boolean {
return true
}
SomeClass (note: used as a dependency in TrackEvent)
class SomeClass #Inject constructor() {
override suspend operator fun invoke(): UserTrackingPropertiesResult {
return UserTrackingPropertiesResult()
}
}
TrackEvent has an entry in an #Module annotated interface because it's an implementation of the UseCase interface:
#Component(modules = [MyModule::class])
interface ShiftsComponent {
fun inject(homeFragment: HomeFragment)
}
#Module
interface MyModule {
#Binds
fun bindsTrackEventUseCase(useCase: TrackEvent): UseCase<Boolean, TrackingEvent>
}
Use Case interfaces
interface UseCase<out T, in P> {
suspend operator fun invoke(params: P): T
}
interface NoParamUseCase<out T> {
suspend operator fun invoke(): T
}
What I'd like to do is to inject an interface into TrackEvent instead of the concrete SomeClass. So I make SomeClass implement a NoParamUseCase
class SomeClass #Inject constructor(): NoParamUseCase<UserTrackingPropertiesResult> {
override suspend operator fun invoke(): UserTrackingPropertiesResult {
return UserTrackingPropertiesResult()
}
}
update TrackEvent to inject the interface:
class TrackEvent #Inject constructor(
private val getTrackingProperties: NoParamUseCase<UserTrackingPropertiesResult>) : UseCase<Boolean, TrackingEvent> {
override suspend operator fun invoke(params: TrackingEvent): Boolean {
return true
}
}
…and update MyModule to inform Dagger of which implementation I'd like to use:
#Module
interface MyModule {
#Binds
fun bindsTrackEventUseCase(useCase: TrackEvent): UseCase<Boolean, TrackingEvent>
// New
#Binds
fun bindsSomeClass(useCase: SomeClass): NoParamUseCase<UserTrackingPropertiesResult>
}
Dagger now claims that there is a missing binding and that I need to declare an #Provides annotated method:
error: [Dagger/MissingBinding] com.myapp.core.domain.usecase.NoParamUseCase<? extends com.myapp.core.tracking.UserTrackingPropertiesResult> cannot be provided without an #Provides-annotated method.
public abstract interface MyComponent {
^
com.myapp.core.domain.usecase.NoParamUseCase<? extends com.myapp.core.tracking.UserTrackingPropertiesResult> is injected at
com.myapp.tasks.tracking.domain.usecase.TrackEvent(getTrackingProperties, …)
…
As far as I can tell, this isn't true:
While, I've opted for #Binds in this instance, replacing this with #Provides and manually providing dependencies here yields the same error.
I'm using the exact same approach for the TrackEvent class and this works.
The only thing I've changed is that I'd like to provide an interface instead. I'd fully understand this error had I not provided the #Binds declaration.
This is different to this question as there's no ambiguity as to which implementation I'm asking Dagger to use in the way that there would be if I had two or more implementations of the same interface.
Why would I get this error now?
According to dagger error message, it expects covariant type NoParamUseCase<? extends UserTrackingPropertiesResult>, but DI module provides invariant NoParamUseCase<UserTrackingPropertiesResult>. To generate appropriate signature for provide method you can change it like this:
#Binds fun bindsSomeClass(useCase: SomeClass): NoParamUseCase<#JvmWildcard UserTrackingPropertiesResult>
After that your code should be compiled successfully.

Is there a way to use injectors in a non-(Activity,Service,Fragment, Application) class

We're using Dagger2 in our application. I am trying to do a room database and I am writing the repository code, but I would like to inject application context and the DAO for the class.
I have a feeling that you can only do Dagger injection in Fragments, Activities, Services, Applications, etc.
Here's what I have:
class DownloadsDataRepositoryImpl : IDownloadsDataRepository, HasAndroidInjector {
#Inject
lateinit var androidInjector : DispatchingAndroidInjector<Any>
#Inject
lateinit var downloadsDao: DownloadsDao
override fun androidInjector(): AndroidInjector<Any> = androidInjector
init {
androidInjector()
}
}
But I'm sure it's not going to work. Is there a way to do it?
As stated, dagger-android is just a tool to help injecting specific framework classes that you can't have control on it's creation.
The proper approach is to use simple construction injection.
To be more direct on how you should expose it on your #Component, I would need more code, specifically on what you have on your activity/fragment, but here is a crude example (that I did not tested, if there are minor errors, you can fix them following the compiler error messages):
First, you will have some object that exposes your DAO. Probably it's room?
#Entity(tableName = "download_table")
data class DownloadEntity(
#PrimaryKey
val key: String
)
#Dao
interface DownloadsDao {
#Query("SELECT * FROM download_table")
fun load(): List<DownloadEntity>
}
#Database(
entities = [DownloadEntity::class], version = 1
)
abstract class DownloadRoomDatabase : RoomDatabase() {
abstract val downloadsDao: DownloadsDao
}
Now we will create a crude repository that is build with #Inject annotation. Dagger will take care of building this object for us. Notice that I am not using dagger-android for it:
interface IDownloadsDataRepository
class DownloadsDataRepositoryImpl #Inject constructor(
val downloadsDao: DownloadsDao
) : IDownloadsDataRepository
How to expose it to your activity/fragment/service requires more details on your implementation. For example, if it's inside a ViewModel or a Presenter that is annotated with #Inject or you are accessing directly on your activity will result in different implementations. Without more details, I will suppose that you are accessing the repository directly on your activity:
class DownloadActivity : FragmentActivity() {
#Inject
lateinit val repo: IDownloadsDataRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerDownloadComponent.factory().create(this).inject(this)
}
}
Now we need to instruct Dagger on how to:
Bind your concrete DownloadsDataRepositoryImpl to the IDownloadsDataRepository interface that the activity requires
How to provide the dependencies to build DownloadsDataRepositoryImpl
For this we will need a module:
#Module
abstract class RepositoryModule {
//We will bind our actual implementation to the IDownloadsDataRepository
#Binds
abstract fun bindRepo(repo: DownloadsDataRepositoryImpl): IDownloadsDataRepository
#Module
companion object {
//We need the database to get access to the DAO
#Provides
#JvmStatic
fun provideDataBase(context: Context): DownloadRoomDatabase =
Room.databaseBuilder(
context,
DownloadRoomDatabase::class.java,
"download_database.db"
).build()
//With the database, we can provide the DAO:
#Provides
#JvmStatic
fun provideDao(db: DownloadRoomDatabase): DownloadsDao = db.downloadsDao
}
}
With this, we can finish the last part of our puzzle, creating the #Component:
#Component(
modules = [
RepositoryModule::class
]
)
interface DownloadComponent {
fun inject(activity: DownloadActivity)
#Component.Factory
interface Factory {
fun create(context: Context): DownloadComponent
}
}
Notice that I did not use any dagger-android code, I don't think it's useful and causes more confusion than necessary. Stick with basic dagger2 constructs and you are fine. You can implement 99.9% of your app only understanding how those constructs works:
#Module, #Component and #Subcomponent
Edit: As stated in the comments, probably you will need to properly manage the scope of your repository, specially the DB creation if you are actually using Room.
Not sure how you implemented dagger, but here is an example how you can provide context to non activity class.
Suppose you have AppModule class, so there you can add provideContext() method:
#Module
class AppModule(app: App) {
private var application: Application = app
#Provides
fun provideContext(): Context {
return application
}
}
and here is non activity class written in Kotlin:
class Utils #inject constructor(private val context: Context) {
..
}
And that's it, just rebuild j
I have a feeling that you can only do Dagger injection in Fragments, Activities, Services, Applications, etc.
You were correct to assume that before Dagger-Android 2.20, but not after 2.20+.
Now you can create a #ContributesAndroidInjector for any class, which will generate an AndroidInjector<T> for that T for which you added #ContributesAndroidInjector.
This means that there is a multi-binding that allows you to get an AndroidInjector<T> for a T, and this is what HasAndroidInjector does for you.
So the following worked for me in a different scenario (for member-injecting Workers in work-manager, instead of creating a multi-binding and a factory):
#Keep
class SyncWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
init {
val injector = context.applicationContext as HasAndroidInjector
injector.androidInjector().inject(this)
}
#Inject
lateinit var apiService: ApiService
and
#ContributesAndroidInjector
abstract fun syncWorker(): SyncWorker
HOWEVER in your particular case, none of this is required.
Dagger-Android is for member-injecting classes using an auto-generated subcomponent, that you typically need only if your injected type is inside a different module, and therefore you can't directly add fun inject(T t) into your AppComponent, OR you don't see your AppComponent.
In your case, simple constructor injection is enough, as you own your own class.
#Singleton
class DownloadsDataRepositoryImpl #Inject constructor(
private val downloadsDao: DownloadsDao
): IDownloadsDataRepository {}
Which you can bind via a module
#Module
abstract class DownloadsModule {
#Binds
abstract fun dataRepository(impl: DownloadsDataRepositoryImpl): IDownloadsDataRepository
}
And otherwise you just create your component instance inside Application.onCreate()
#Component(modules = [DownloadsModule::class])
#Singleton
interface AppComponent {
fun dataRepository(): DownloadsDataRepository
#Component.Factory
interface Factory {
fun create(#BindsInstance appContext: Context): AppComponent
}
}
And
class CustomApplication: Application() {
lateinit var component: AppComponent
private set
override fun onCreate() {
super.onCreate()
component = DaggerAppComponent.factory().create(this)
}
}
Then you can get it as
val component = (context.applicationContext as CustomApplication).component
Though technically you may as well create an extension function
val Context.appComponent: AppComponent
get() = (applicationContext as CustomApplication).component
val component = context.appComponent

Dagger2 creating a map of concrete instances fails with "cannot be provided without an #Provides-annotate"

I am playing around with Dagger collection, in particular with map.
I want to use Dagger2 to inject a map whose key is an enum and the values a concrete
implementation of an interface. The map
is injected in a presenter. An unique component instantiates the presenter, an
activity uses the instantiation to display the strings produced by the value
of the map.
I get the following error:
e: ../DaggerMap/app/build/tmp/kapt3/stubs/debug/com/aklal/briquedagger2/MainComponent.java:7: error: [Dagger/MissingBinding] java.util.Map<com.aklal.briquedagger2.Lapse.TIME,? extends com.aklal.briquedagger2.Lapse.ChristmasTime> cannot be provided without an #Provides-annotated method.
public abstract interface MainComponent {
^
java.util.Map<com.aklal.briquedagger2.Lapse.TIME,? extends com.aklal.briquedagger2.Lapse.ChristmasTime> is injected at
com.aklal.briquedagger2.MainPresenter(mapOfLapse)
com.aklal.briquedagger2.MainPresenter is injected at
com.aklal.briquedagger2.MainModule.getMainPresenter(connection)
com.aklal.briquedagger2.Presenter is provided at
com.aklal.briquedagger2.MainComponent.presenter()
FAILURE: Build failed with an exception.
The "project" can be found here
Implementation
I have an interface ChristmasTime which is implemented by two classes:
TimeUntilNextChristmas
TimeSinceLastChristmas
These implementation are similarly simply defined as follow:
class TimeSinceLastChristmas #Inject constructor(): ChristmasTime {
override fun getLapseOfTime() = "SINCE TEST"
}
I want to let Dagger2 create a map with
an enum value as key
ChristmasTime as type value
The key value is defined as follow:
#MapKey
annotation class TimeKey(val value: TIME)
enum class TIME {
UNTIL,
SINCE
}
I created a module to provide concrete implementations of type ChristmasTime:
#Module
interface LapseOfTimeModule {
#Binds
#IntoMap
#TimeKey(TIME.UNTIL)
fun provideLapsesOfTimeUntil(t: TimeUntilNextChristmas): ChristmasTime
#Binds
#IntoMap
#TimeKey(TIME.SINCE)
fun provideLapsesOfTimeSince(t: TimeSinceLastChristmas): ChristmasTime
}
I want to displayed the string returned by the concrete implementations on the
screen. To do so, a presenter communicates with an activity the strings contained
in the map (that has been injected in the presenter):
class MainPresenter #Inject constructor(
private val mapOfLapse: Map<TIME, ChristmasTime>
) : Presenter {
override fun getDateUntil(): String = mapOfLapse[TIME.UNTIL]?.getLapseOfTime() ?: "UNTIL FAILED"
}
The component to instantiate the presenter in the MainActivity takes the module that defines the map (LapseOfTimeModule) and a MainModule
#Component(modules = [MainModule::class, LapseOfTimeModule::class])
interface MainComponent {
fun presenter(): Presenter
}
MainModule is:
#Module
interface MainModule {
#Binds
fun getMainPresenter(connection: MainPresenter): Presenter
}
And the MainActivity is:
class MainActivity : AppCompatActivity() {
#Inject
lateinit var presenter: Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter = DaggerMainComponent.create().presenter()
bttDisplayNewDate.setOnClickListener{
displayDate(
presenter.getDateSince(),
presenter.getDateUntil()
)
}
}
fun displayDate(since: String, until: String) {
tvSince.text = since
tvUntil.text = until
}
}
Does anyone know how to fix that ?
Here are some threads that I read but they did not help much:
Kotlin dagger 2 Android ViewModel injection error
Dagger2 Inherited subcomponent multibindings
Thanks in advance!!
ps: The version of dagger used is 2.24 and kotlin version is 1.3.41
It's a little bit tricky how it works. In Java it will work. Kotlin decompile Map<K,V> just to Map, and Dagger can't find a Map without types of K and V. To fix it please use just java.util.Map for autogenerated daggers class.
class MainPresenter #Inject constructor(
private val mapOfLapse: java.util.Map<TIME, ChristmasTime>
) : Presenter {
of course, then you need to map it into kotlin map to have all extension functions.

Can't Provide Qualified String with Dagger2

I'm trying to follow the sample here
I have already used Dagger2 with the AndroidInjector successfully but now I was experimenting the new DaggerApplication and DaggerAppCompatActivity.
I get the error:
Error:(5, 1) error: [dagger.android.AndroidInjector.inject(T)]
java.lang.String cannot be provided without an #Inject constructor or
from an #Provides- or #Produces-annotated method.
I don't think that the new Dagger classes are causing the issue.
If I remove the #DeviceModel in BuildModule.kt the code compiles.
Any suggestion?
Here the code:
The AppComponent.kt
#Component(modules = arrayOf(
AndroidSupportInjectionModule::class,
AppModule::class,
MainActivitySubComponent.MainActivityModule::class
))
interface AppComponent : AndroidInjector<App> {
#Component.Builder
abstract class Builder : AndroidInjector.Builder<App>()
}
The AppModule.kt
#Module(subcomponents = arrayOf(MainActivitySubComponent::class))
class AppModule {
}
The BuildModule.kt
#Module
class BuildModule {
#Provides
#DeviceModel
fun provideModel(): String {
return MODEL
}
}
The DeviceModel.kt
#Qualifier
#Retention(AnnotationRetention.RUNTIME)
#MustBeDocumented
annotation class DeviceModel
The MainActivitySubComponent.kt
#Subcomponent(modules = arrayOf(BuildModule::class))
interface MainActivitySubComponent : AndroidInjector<MainActivity> {
#Subcomponent.Builder
abstract class Builder : AndroidInjector.Builder<MainActivity>()
#Module(subcomponents = arrayOf(MainActivitySubComponent::class))
abstract class MainActivityModule {
#Binds
#IntoMap
#ActivityKey(MainActivity::class)
internal abstract fun bind(builder: MainActivitySubComponent.Builder): AndroidInjector.Factory<out Activity>
}
}
The *MainActivity.kt**
class MainActivity : DaggerAppCompatActivity() {
#Inject
#DeviceModel
lateinit var model: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(model, model);
}
}
The App.kt
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<App> {
return DaggerAppComponent.builder().create(this)
}
}
When using Dagger with Kotlin objects, we have to consider how Kotlin actually creates properties in the JVM.
#Inject
#DeviceModel
lateinit var model: String
With a property like this, Kotlin compiles three Java elements: a private backing field, a getter, and a setter (due to var rather than val). With no further clues as to your intent, Kotlin places your DeviceModel annotation on the getter.
The solution is to specify that the field is to be annotated with #field:[annotation].
#Inject
#field:DeviceModel
lateinit var model: String
You may also need to use #JvmField to make the backing field public for Dagger, which would preclude using lateinit, and of course, require you to initialize the String.
#Inject
#field:DeviceModel
#JvmField
var model: String
Update: I'm not sure why, but in my testing, the field was private, which is why I suggested #JvmField. According to the Kotlin documentation, the field should be created with the same visibility as the setter.
Note that in general, Kotlin is smart enough to apply custom annotations with #Target(AnnotationTarget.FIELD) to the backing field, but this would prevent you from also using it on the fun providesModel

Categories

Resources