Dagger 2 can't inject from subcomponent - android

I know that generally it shouldn't make a difference that this is using Kotlin, but I've run into odd cases where the #Named qualifier needed a scope in Kotlin.
I have a ViewHolderFactory class that allows me to create a simple mapping of view type -> view holder class:
#Singleton
class ViewHolderFactoryImpl #Inject constructor(
private val viewHolderComponentProvider: Provider<ViewHolderSubcomponent.Builder>
): ViewHolderFactory(mapOf(
R.layout.view_error to ErrorViewHolder::class.java,
R.layout.view_soft_error to SoftErrorViewHolder::class.java,
R.layout.view_empty to EmptyViewHolder::class.java,
R.layout.view_loading to LoadingViewHolder::class.java,
R.layout.item_got_it to GotItViewHolder::class.java)) {
override fun createViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewHolder = super.createViewHolder(parent, viewType)
if (viewHolder is Injectable) {
viewHolderComponentProvider.get()
.viewHolder(viewHolder)
.build()
.inject(viewHolder)
}
return viewHolder
}
}
The ViewHolderSubcomponent is defined below, the goal is to be able to create one subcomponent for each view holder and inject a few things:
#ViewHolderScope
#Subcomponent(modules = [ViewHolderModule::class])
interface ViewHolderSubcomponent {
fun inject(viewHolder: RecyclerView.ViewHolder)
fun viewHolder(): RecyclerView.ViewHolder
#Subcomponent.Builder
interface Builder {
#BindsInstance
fun viewHolder(viewHolder: RecyclerView.ViewHolder): Builder
fun build(): ViewHolderSubcomponent
}
}
The ViewHolderModule is defined as:
#Module
class ViewHolderModule {
#Provides #ViewHolderScope
fun provideSectionTitleViewHolder(viewHolder: RecyclerView.ViewHolder): SectionTitleViewHolder =
SectionTitleViewHolder(viewHolder.itemView)
}
When I run the app I find that the injection didn't work, my #Inject lateinit var values are null. Looking at the generated code I can see why:
#Override
public void inject(RecyclerView.ViewHolder viewHolder) {
MembersInjectors.<RecyclerView.ViewHolder>noOp().injectMembers(viewHolder);
}
There's no MembersInjectors<RecyclerView.ViewHolder> created for this subcomponent. I can't figure out how to get this to work. I know that I should be able to inject into objects not created by dagger, I just can't quite figure out what I'm missing here.
Oh, if it helps any, I did make sure to include the ViewHolderSubcomponent in my AppModule's list of subcomponents

inject(viewHolder: RecyclerView.ViewHolder) will always be a no-op, because framework classes (or most libraries in this case) don't have any #Inject annotated fields. Dagger will only generate code for the class named in your inject(MyClass instance) methods, not for any of its sub-types.
So if you have a ErrorViewHolder : RecyclerView.ViewHolder, then you have to use a component that has a inject(ErrorViewHolder instance) method to generate code to inject ErrorViewHolder.
To clarify, because it's generated code—and not dynamic reflection at runtime—calling inject(viewHolder: RecyclerView.ViewHolder) like you do with an viewHolder : ErrorViewHolder will still try to inject fields for RecyclerView.ViewHolder, not ErrorViewHolder. And RecyclerView.ViewHolder will always be a no-op as mentioned.
You will have to modify your setup quite a bit so that you can provide a specific subcomponent that can inject a specific viewholder, you can't use one "generic" component for different types. You could create a base class between RecyclerView.ViewHolder and ErrorViewHolder, but then again, you could only inject fields declared (and #Inject annotated) in your base class, not the specific child.

The Kotlin proptery itself is not known to Dagger. What you are trying to achieve could be handled as a field or setter injection. Since a setter is generated with any property I usually go for #set:Inject.
#set:Inject lateinit var myVar: Type
Alternatively you can consider constructor injections. Like this you can define the properties as val and private.

Related

How to inject a ViewModel to another Viewmodel using Hilt

I have a word quiz app where there are different word games (multiple choice, spelling, match the words, etc). I have created a ParentViewModel to keep the common things, such as getting words from Room database, current question variable, etc. But, on the other hand, as each game has different aspects. They have their own viewmodels. I want to be able to use functions and variables from ParentViewModel in child view models. My question is if I am correctly injecting the ParentViewModel below or if there is another way to do this?
ParentViewModel:
#HiltViewModel
class ParentViewModel #Inject constructor(
private val wordRepository: WordRepository
): ViewModel(){
// Common things such as getting words from database.
}
Child view model (Different view model for each type of game):
#HiltViewModel
class SpellingViewModel #Inject constructor(
practiceViewModel: PracticeViewModel
): ViewModel(){
// Functions and varibles spesific to the current game.
}
Appmodule:
#Provides
#Singleton
fun provideParentViewModel(wordRepository: WordRepository): ParentViewModel {
return ParentViewModel(wordRepository)
}
I think a baseViewModel should be created and inherit child classes from the parent class, You can create open functions that are in the parent class and use them in the child classes.
Let me give you an example :
abstract class BaseViewModel : ViewModel(){open fun wordGameOne(){
// do something
}
open fun wordGameTwo(){
// do something
}
}
And in the child class, you can override
#HiltViewModel class GameOneViewModel #Inject constructor(
private val wordRepository: WordRepository) : BaseViewModel() {
override fun wordGameOne(){
// do something
}
}

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 multibinding with a custom qualifier

I have the following interface called SettingHandler that is responsible for handling events related to a particular setting inside the Android app.
interface SettingHandler {
fun onHandleEvent(event: SettingEvent)
fun candleHandleEvent(event: SettingEvent): Boolean
fun getSettingId(): SettingId
}
Then let's say I have the following Dagger module:
#Module
object SettingsModule {
#Provides
fun provideSettingPresenter(
view: SettingsContract.View,
settingHandlers: Set<#JvmSuppressWildcards SettingHandler>
): SettingsPresenter {
//
}
#Provides
#IntoSet
fun provideSettingHandlerA(
dependencyA: A,
dependencyB: B
): SettingHandler {
//
}
#Provides
#IntoSet
fun provideSettingHandlerB(
settingHandlerC: Provider<SettingHandler>
): SettingHandler {
//
}
#Provides
#IntoSet
fun provideSettingHandlerC(
settingHandlerB: Provider<SettingHandler>
): SettingHandler {
//
}
}
As you can see, nothing too special here, expect for the SettingHandlerB and SettingHandlerC provider methods. They both depend on each other, so I've decided to resolve this circular dependency using the Dagger supplied Provider class. Since I require a concrete implementation of a setting handler (SettingHandlerC for the SettingHandlerB and SettingHandlerB for the SettingHandlerC), I now need to differentiate between them. That's where a #Qualifier annotation comes into play. I created the following qualifier annotation to differentiate among different implementations.
#Qualifier
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
annotation class SettingHandlerType(val id: SettingId)
SettingId is basically an enumeration that contains all possible setting constants. So now my SettingHandlers module looks as follows:
#Module
object SettingsModule {
#Provides
fun provideSettingPresenter(
view: SettingsContract.View,
settingHandlers: Set<#JvmSuppressWildcards SettingHandler>
): SettingsPresenter {
//
}
#Provides
#SettingHandlerType(SettingId.A)
fun provideSettingHandlerA(
dependencyA: A,
dependencyB: B
): SettingHandler {
//
}
#Provides
#SettingHandlerType(SettingId.B)
fun provideSettingHandlerB(
#SettingHandlerType(SettingId.C)
settingHandlerC: Provider<SettingHandler>
): SettingHandler {
//
}
#Provides
#SettingHandlerType(SettingId.C)
fun provideSettingHandlerC(
#SettingHandlerType(SettingId.B)
settingHandlerB: Provider<SettingHandler>
): SettingHandler {
//
}
}
And here comes the problem. Mutlibinding now does not work, because all my SettingHandlers are annotated with a #SettingHandlerType annotation and a Set that I'm injecting into the SettingsPresenter also needs to be annotated. However, annotating it with, for example, #SettingHandlerType(SettingId.A) won't work because in that case the Set will contain only setting handlers with that particular qualifier (SettingHandlerA, in this case). How can I construct a Set data structure using multibinding because I really don't want to provide another provider method where I'll be constructing it myself?
Any help would be much appreciated. Thanks in advance.
You want two different types of data: Individual handlers of type #SettingHandlerType(/*...*/) SettingHandler and a set of type Set<SettingHandler>. I think the best way would be to have each definition come with a #Binds #IntoSet complement.
#Binds #IntoSet abstract fun bindSettingHandlerA(
#SettingHandlerType(SettingId.A) handler: SettingHandler): SettingHandler
Unfortunately, since you've defined this as an object in Kotlin, it's slightly more complicated to put the necessarily-abstract #Binds methods on the same object. You may need to pursue one of these other solutions to make that topology work with your case, including the use of a companion object to capture the otherwise-static #Provides methods:
Dagger 2 static provider methods in kotlin
#Provides and #Binds methods in same class Kotlin
Dagger2 : How to use #Provides and #Binds in same module (java)

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 we inject "View" in Presenter using Toothpick as in Dagger?

In Dagger, You can inject your activity as View in Presenter, Please follow below example,
Splash Module
#Module
class SplashModule {
#Provides
fun provideXUseCase(
xRepository: XRepository
) = XUseCase(xRepository)
#Provides
fun provideSplashPresenter(
view: SplashView,
xUseCase: XUseCase
): SplashPresenter = SplashPresenter(
view,
xUseCase
)
}
View Module
#Module
abstract class ViewModule {
#Binds
abstract fun provideSplashView(activity: SplashActivity): SplashView
}
Activity Module
#Module
abstract class ActivitiesModule {
#ContributesAndroidInjector(modules = [SplashModule::class, ViewModule::class])
abstract fun bindSplashActivity(): SplashActivity
}
I tried to find how to do it in ToothPick, but could not find any official document or blog post!
Thanks 🙏
Yes, you can do it in a very similar fashion.
You can have a module that binds the view interface to an InstanceProvider (which you can define as a lambda)
In the presenter you declare the View as #Inject and then call Toothpick.inject() as part of the initialization.
The only tricky part is to take care of the scope tree. When I did this I used an Application scope as well as an Activity scope and only declared the bind of the View at Activity level, then the presenter calls inject with the same scope and it all should work fine.
The Activity scope is needed so we override the InstanceProvider each time a new Activity is created (and the View has a new reference and I recall the old one was cached if the scope was the same)
I hope I explained it properly. It wasn't obvious how to do it, but once all the pieces are in place it makes sense.

Categories

Resources