(using kotlin) I have an app that uses a settings activity with 2 fragments. I'd like both to get the same instance of the SettingsViewModel as the activity. I assume there's a scoping issue that I'm missing.
First, I have the standard ViewModelModule:
#Module
abstract class ViewModelModule {
#Binds #IntoMap
#ViewModelKey(SettingsViewModel::class)
abstract fun bindSettingsViewModel(viewModel: SettingsViewModel): ViewModel
#Binds
abstract fun bindViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
}
I bind my activities in:
#Module
abstract class AndroidBindingModule {
#PerActivity
#ContributesAndroidInjector(modules = [SettingsActivityModule::class])
abstract fun contributeSettingsActivity(): SettingsActivity
}
With all other things in place this works well and SettingsActivity does get an instance of the SettingsViewModel. SettingsActivityModule adds the following:
#PerFragment
#ContributesAndroidInjector
abstract fun contributeMainSettingsFragment(): MainSettingsFragment
#PerFragment
#ContributesAndroidInjector
abstract fun contributeDebugSettingsFragment(): DebugSettingsFragment
Both fragments appear to have the injectors called on them (I've checked through the debugger and AndroidSupportInjection.inject(fragment) is called). The fragments include:
#Inject lateinit var mainViewModel: SettingsViewModel
But in my fragment's onCreate() I'm seeing that mainViewModel is still null. Is there anything special I need to do here to avoid calling ViewModelProviders.of(activity)[SettingsViewModel::class.java] and instead injecting the view model?
UPDATE:
After a bit more reading I found that the correct way of using the view model injection in fragments is to inject the factory and get the view model in onActivityCreated:
#Inject lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var mainViewModel: SettingsViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mainViewModel = ViewModelProviders.of(activity, viewModelFactory)[SettingsViewModel::class.java]
}
This makes sense since I have MyViewModelFactory bound as the ViewModelProvider.Factory and it's annotated with #Singleton. When I try to compile the above I get the following error:
Error:(6, 1) error: [dagger.android.AndroidInjector.inject(T)] java.util.Map<kotlin.reflect.KClass<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>> cannot be provided without an #Provides-annotated method.
It appears that Dagger can't find the mapping created by the ViewModelModule. I'm still at a loss at how that can be. Maybe my tree is incorrect? Why would activities in AndroidBindingModule be able to get the ViewModel but not fragments?
AppComponent
- AndroidInjectionModule
- AndroidBindingModule
- AppModule
- SdkModule
- ViewModelModule
- GotItCardModule
- ViewHolderSubcomponent (provides a mapping of layout ID -> ViewHolder for a factory)
UPDATE
I've done a little more digging into this... From the full error:
e: /home/user/workspace/Example/sdktest/build/tmp/kapt3/stubs/debug/com/example/sdktest/di/AppComponent.java:6: error: [dagger.android.AndroidInjector.inject(T)] java.util.Map<kotlin.reflect.KClass<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>> cannot be provided without an #Provides-annotated method.
e:
e: public abstract interface AppComponent {
e: ^
e: java.util.Map<kotlin.reflect.KClass<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>> is injected at
e: com.example.sdktest.di.viewmodel.ExampleViewModelFactory.<init>(creators)
e: com.example.sdktest.di.viewmodel.ExampleViewModelFactory is injected at
e: com.example.sdktest.di.viewmodel.ViewModelModule.bindViewModelFactory(factory)
e: android.arch.lifecycle.ViewModelProvider.Factory is injected at
e: com.example.sdktest.ui.settings.fragment.MainSettingsFragment.viewModelFactory
e: com.example.sdktest.ui.settings.fragment.MainSettingsFragment is injected at
e: dagger.android.AndroidInjector.inject(arg0)
I think the issue is that somehow Dagger is trying to inject my fragment with dagger.android.AndroidInjecton instead of dagger.android.AndroidSupportInjection. Still not sure how to fix.
OK, so I found the answer and it wasn't anywhere near what I thought. My translation of the GithubViewModelFactory into Kotlin included the following constructor:
#Singleton
class MetaverseViewModelFactory #Inject constructor(
creators: Map<KClass<out ViewModel>, Provider<ViewModel>>
): ViewModelProvider.Factory {
private val creators: Map<Class<out ViewModel>, Provider<ViewModel>> =
creators.mapKeys { it.key.java }
//...
}
This was due to the ViewModelKey in Kotlin being able to use KClass only instead of Class. It turns out that kapt takes care of this and the correct factory should look like:
#Singleton
class MetaverseViewModelFactory #Inject constructor(
private val creators: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
): ViewModelProvider.Factory {
//...
}
Note the additional #JvmSupressWildcards to also avoid turning Provider<ViewModel> into Provider<? extends ViewModel>
Related
I'm using the well-known Dagger-ViewModelFactory pattern to be able to inject a factory for all the ViewModel in all the activities.
#ActivityScope
class ViewModelFactory #Inject constructor(
private val creators: MutableMap<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
return creator.get() as T
}
}
The problem I have is that when I inject the factory into an Activity Dagger fails because the providers of the objects for the ViewModels that I'm not going to use are not always accessible. They are not because the modules that contain the providers have not been added.
For example, I have a LogIn activity and a SignUp activity, and this is the way I add the subcomponents for them:
#ContributesAndroidInjector(modules = [
ViewModelModule::class,
FirebaseModule::class,
LogInModule::class,
BindLogInModule::class
])
#ActivityScope
internal abstract fun loginActivityInjector(): LoginActivity
#ContributesAndroidInjector(modules = [
ViewModelModule::class,
FirebaseModule::class,
SignUpModule::class,
BindSignUpModule::class
])
#ActivityScope
internal abstract fun signUpActivityInjector(): SignUpActivity
Please notice that when I create the subcomponent for SignUpActivity I do not add the Module LogInModule because I do not need the bindings in that Module.
The result is that I get the error
e: com.package.my.AppComponent.java:8: error: [Dagger/MissingBinding] com.package.my.login.domain.LogInAuthenticator cannot be provided without an #Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector {
^
A binding with matching key exists in component: com.package.my.di.ActivityInjectorsModule_LoginActivityInjector$app_prodDebug.LoginActivitySubcomponent
com.package.my.login.domain.LogInAuthenticator is injected at
com.package.my.login.repository.LoginRepository(logInAuthenticator)
com.package.my.login.repository.LoginRepository is injected at
com.package.my.login.domain.LoginUseCase(loginRepository)
com.package.my.login.domain.LoginUseCase is injected at
com.package.my.login.presentation.LoginViewModel(loginUseCase)
com.package.my.login.presentation.LoginViewModel is injected at
com.package.my.di.ViewModelModule.provideLoginViewModel(viewModel)
java.util.Map,javax.inject.Provider> is injected at
com.package.my.di.ViewModelFactory(creators)
com.package.my.di.ViewModelFactory is injected at
com.package.my.di.ViewModelModule.bindViewModelFactory$app_prodDebug(factory)
androidx.lifecycle.ViewModelProvider.Factory is injected at
com.package.my.login.ui.SignUpActivity.viewModelFactory
com.package.my.login.ui.SignUpActivity is injected at
dagger.android.AndroidInjector.inject(T) [com.package.my.di.AppComponent → com.package.my.di.ActivityInjectorsModule_SignUpActivityInjector$app_prodDebug.SignUpActivitySubcomponent]
This happens because LogInAuthenticator is provided by LogInModule.
Does this mean that the only solution is to add LogInModule even if I don't really need to create GoogleSignInClient in the SignUpActivity?
You have declared both of #ContributesAndroidInjector methods to be dependent on ViewModelModule. Inside ViewModelModule you have declared all of the ViewModels out there, which means, that at the point when Dagger wants to construct the dependency tree for SignUpActivity it will also require you to explicitly mention how LoginViewModel should be constructed. This happens, because Dagger needs to know how each of the dependency declared inside ViewModelModule should be constructed.
The solution for you case will be either include all of the modules in all of #ContributesAndroidInjector declarations (which is an ugly approach), or, alternatively, move the provider method of SignUpViewModel to SignUpModule and do not include ViewModelModule for SignUpActivity declaration.
Here's the setup that works for me.
First, I have created a BaseActivityModule, which all of feature modules should include in their dedicated #Module classes:
#Module
abstract class BaseActivityModule {
#Binds abstract fun bindsViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
}
Then, assuming we have 2 features: Foo and Bar:
#Module
abstract class ActivitiesModule {
#PerActivity #ContributesAndroidInjector(modules = [FooModule::class])
abstract fun contributesFooActivity(): FooActivity
#PerActivity #ContributesAndroidInjector(modules = [BarModule::class])
abstract fun contributesBarActivity(): BarActivity
}
The implementation class of ViewModelProvider.Factory should be scoped with #PerActivity because the same instance of ViewModelProvider.Factory should be provided each time that dependency is needed to be injected in the scope of particular activity:
private typealias ViewModelProvidersMap = Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
#PerActivity
class MyViewModelFactory #Inject constructor(
private val creators: ViewModelProvidersMap
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
var viewModelProvider = creators[modelClass]
if (viewModelProvider == null) {
val entries = creators.entries
val mapEntry = entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
} ?: throw IllegalArgumentException("Unknown model class $modelClass")
viewModelProvider = mapEntry.value
}
try {
#Suppress("UNCHECKED_CAST")
return viewModelProvider.get() as T
} catch (e: Throwable) {
throw IllegalArgumentException("Couldn't create ViewModel with specified class $modelClass", e)
}
}
}
Where #PerActivity is declared this way:
#Scope
#Retention(AnnotationRetention.RUNTIME)
annotation class PerActivity
FooModule and BarModule are declared as such:
#Module(includes = [BaseActivityModule::class])
abstract class FooModule {
#Binds #IntoMap #ViewModelKey(FooViewModel::class)
abstract fun bindsFooViewModel(viewModel: FooViewModel): ViewModel
}
#Module(includes = [BaseActivityModule::class])
abstract class BarModule {
#Binds #IntoMap #ViewModelKey(BarViewModel::class)
abstract fun bindsBarViewModel(viewModel: BarViewModel): ViewModel
}
Then we are including ActivitiesModule in the AppComponent as such:
#Singleton
#Component(modules = [
AndroidInjectionModule::class,
ActivitiesModule::class
])
interface AppComponent {
...
}
With this approach we've moved the ViewModelProvider.Factory creation one layer down: previously it was in the topmost AppComponent and now each of subcomponents will take care of creating the ViewModelProvider.Factory.
The answer to why you need to add the LoginModule lies in your error log. The error log traces the dependencies as below:
SignUpActivity <--is injected at-- ViewModelFactory <--is injected at-- LoginViewModel <--is injected at-- LoginUseCase <--is injected at-- LoginRepository <--is injected at-- LoginAuthenticator
The map above shows that you need to add LoginModule because Dagger needs it to successfully inject your ViewModelFactory into your SignUpActivity.
EDIT
Move SignUpViewModel binding from your ViewModelModule and place it in your SignUpModule like below:
#Module(includes = [SignUpModule.BindsModule::class])
class SignUpModule {
// your other provides methods
#Module
interface BindsModule{
#Binds
#IntoMap
#ViewModelKey(SignUpViewModel::class)
fun signUpViewModel(signUpViewModel: SignUpViewModel): ViewModel
}
}
Then add subcomponents this way. Notice ViewModelModule has been excluded
#ContributesAndroidInjector(modules = [
FirebaseModule::class,
SignUpModule::class,
BindSignUpModule::class
])
#ActivityScope
internal abstract fun signUpActivityInjector(): SignUpActivity
I have followed Android Architecture Blueprints Dagger2 for dependency injection: URL
Now I want to inject Adapter to my Fragment class:
#ActivityScoped
class MainFragment #Inject
constructor(): DaggerFragment(), ArtistClickCallback {
#Inject lateinit var adapter : ArtistAdapter
}
Main Module class:
#Module
abstract class MainModule {
#FragmentScoped
#ContributesAndroidInjector(modules = [MainFragmentModule::class])
internal abstract fun mainFragment(): MainFragment
#Binds
internal abstract fun bindArtistClickCallback(mainFragment: MainFragment) : ArtistClickCallback
}
MainFragmentModule:
#Module
class MainFragmentModule {
#Provides
fun provideArtistAdapter() = ArtistAdapter()
}
And this is my adapter class:
class ArtistAdapter #Inject constructor(
private val artistClickCallback : ArtistClickCallback
) : PagedListAdapter<LastFmArtist, RecyclerView.ViewHolder>(POST_COMPARATOR)
When I build the project I get following Kotlin compiler error:
error: [Dagger/DependencyCycle] Found a dependency cycle:
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.sample.android.lastfm.LastFmApp> {
^
com.sample.android.lastfm.ui.main.MainFragment is injected at
com.sample.android.lastfm.ui.main.MainModule.bindArtistClickCallback$app_debug(mainFragment)
com.sample.android.lastfm.ArtistClickCallback is injected at
com.sample.android.lastfm.ui.main.ArtistAdapter.artistClickCallback
com.sample.android.lastfm.ui.main.ArtistAdapter is injected at
com.sample.android.lastfm.ui.main.MainFragment.adapter
com.sample.android.lastfm.ui.main.MainFragment is injected at
com.sample.android.lastfm.ui.main.MainActivity.mainFragment
com.sample.android.lastfm.ui.main.MainActivity is injected at
dagger.android.AndroidInjector.inject(T) [com.sample.android.lastfm.di.AppComponent → com.sample.android.lastfm.di.ActivityBindingModule_MainActivity$app_debug.MainActivitySubcomponent]
Can you suggest me how to solve this problem?
Codes can be found at URL
Your fragment should probably not have #ActivityScoped as a scope. Further do not use constructor injection with fragments (or any other framework type)! The Android framework will create those objects in some cases, and you will end up with the wrong reference in your classes. Add the fragment to the corresponding component via its builder.
Also you're using a provides annotated method as well as constructor injection (#Inject constructor()). Pick one. Since you also use field injection within the ArtistAdapter the next "error" you would encounter would be a null callback because you don't inject the adapter anywhere. You just create the object.
Constructor injection should usually be favored, which will also inject fields. Remove the following completely, keep the annotation on the construcor:
#Provides
fun provideArtistAdapter() = ArtistAdapter()
Moving on, your error originates in MainActivitySubcomponent (last line) and seems to be because your MainFragment is bound as an ArtistClickCallback, but requires a ArtistAdapter which requires a ArtistClickCallback...hence your dependency cycle.
This issue should resolve itself once you fix the problems mentioned (#Inject constructor on the fragment in this case) above, since it originates through the MainFragment being constructed by Dagger within the MainActivitySubcomponent, which is the wrong place anyways since your fragment should have a lower scope than the Activity.
Further you need to move your binding (#Binds fun bindArtistClickCallback) into the MainFragmentModule, since there is no fragment to bind in the Activity component (where you add the binding currently)
When you fix all those issues, you will bind your fragment to the correct FragmentSubcomponent, where you will bind it as a Callback, with which you can then create the Adapter and it should work.
I recommend you have a more thorough look on Dagger and make sure to understand all the issues / fixes pointed out.
This is how it should look
#FragmentScoped
class MainFragment(): DaggerFragment(), ArtistClickCallback {
#Inject lateinit var adapter : ArtistAdapter
}
#Module
abstract class MainModule {
#FragmentScoped
#ContributesAndroidInjector(modules = [MainFragmentModule::class])
internal abstract fun mainFragment(): MainFragment
}
#Module
class MainFragmentModule {
#Binds
internal abstract fun bindArtistClickCallback(mainFragment: MainFragment) : ArtistClickCallback
}
class ArtistAdapter #Inject constructor(
private val artistClickCallback : ArtistClickCallback
) : PagedListAdapter<LastFmArtist, RecyclerView.ViewHolder>(POST_COMPARATOR)
I've started using Dagger2, so there's still a lot to learn. I'm wondering if someone could point me on the right direction.
So, I've created a module for registering the view models used by my activities. It looks like this:
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(MainActivityViewModel::class)
internal abstract fun bindMainActivityViewModel(viewModel: MainActivityViewModel): ViewModel
#Binds
#IntoMap
#ViewModelKey(ShowDetailsViewModel::class)
abstract fun bindShowDetaislViewModel(viewModel: ShowDetailsViewModel): ViewModel
}
ViewModelKey is a simple helper annotation class which looks like this:
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class ViewModelKey (val value: KClass<out ViewModel>) {
}
The ViewModelModule is loaded by my main app component (used for creating the app):
#Singleton
#Component(
modules=[
AndroidSupportInjectionModule::class,
AppModule::class,
DatabaseModule::class,
NewsServiceModule::class,
JobBindingModule::class,
ViewModelModule::class,
PreferencesModule::class,
ActivityBindingModule::class
]
)
interface AppComponent: AndroidInjector<MyApp> {
#Component.Builder
abstract class Builder: AndroidInjector.Builder<MyApp>()
}
And here's the code for the ActivityBindingModule, responsible for setting up the subcomponents (in this case, activities used by my app):
#Module
abstract class ActivityBindingModule {
#ActivityScoped
#ContributesAndroidInjector()
internal abstract fun mainActivity(): MainActivity
#ActivityScoped
#ContributesAndroidInjector
internal abstract fun showDetailsActivity(): ShowDetailsActivity
}
Internally, each activity is instantiating the view model with code that looks like this (called from within the onCreate method):
//view model code
_viewModel = ViewModelProviders.of(this, viewModelFactory)[ShowDetailsViewModel::class.java]
And, as you'd expect, viewModelFactory is injected as field:
#Inject lateinit var viewModelFactory: ViewModelProvider.Factory
Both view models have external dependencies which are set up on the other modules referenced by the top app component.
And, for the sake of completeness, here's the code for my view model factory:
#Singleton
class ViewModelFactory #Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T
= viewModels[modelClass]?.get() as T
This code works, but it seems like it can be improved. After reading the docs, I'm under the impression that I could refactor my ViewModeModule so that it will simply instantiate my ViewModelFactory and move each of the view model declarations into separate module (so that each of them can be injected only in the "correct" activity).
In order to test this, I've started by moving the ShowDetailsViewModel into a new module which has only one entry:
#Module
internal abstract class DetailsModule {
#Binds
#IntoMap
#ViewModelKey(ShowDetailsViewModel::class)
abstract fun bindShowDetaislViewModel(viewModel: ShowDetailsViewModel): ViewModel
}
After that, the ViewModelModule looks like this:
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(MainActivityViewModel::class)
internal abstract fun bindMainActivityViewModel(viewModel: MainActivityViewModel): ViewModel
}
And I've updated the ActivityBindingModule so that in looks like this:
#Module
abstract class ActivityBindingModule {
#ActivityScoped
#ContributesAndroidInjector()
internal abstract fun mainActivity(): MainActivity
#ActivityScoped
#ContributesAndroidInjector(modules = [DetailsModule::class])
internal abstract fun showDetailsActivity(): ShowDetailsActivity
}
Notice that now I'm passing the DetailsModule (which instantiates the ShowDetailsViewModel) to the ContributeAndroidInjector annotation which is applied to the showDetailsActivity method because that view model is only used by that activity.
Now, I'm surely missing something because after doing this, I'm always getting the following exception:
java.lang.IllegalStateException: ViewModelProviders.of(th…ilsViewModel::class.java] must not be null
If I debug the app, I can see that moving the ShowDetailsViewModel into its own model does not register it on the map used by the factory (ie, the map has only one entry, which corresponds to the MainActivityViewModel that is registered in the ViewModelModule.
I thought that moving each view model the declaration into each a module used by a subcomponent should still allow it to be registered in a map injected by a module which is registered with the top component. Am I wrong? What is it that I'm missing to make this work?
Thanks.
The problem lies with ViewModelFactory being #Singleton and that it won't get any of the bindings you add in your subcomponents. Remove the scope from your factory or make it #ActivityScoped (the same scope as the ViewModel for the Activity)
The Activity (#ActivityScoped) has access to the factory (#Singleton), but the factory (#Singleton) does not have access to use or create the ViewModel from a lower scope (#ActivityScoped). So moving the factory to the same scope (#ActivityScoped) would give it access to create the viewmodel in question.
I need to #Inject constructor activity and fragmentManager into my Navigator.class. But I'm getting this error:
AppCompatActivity cannot be provided without an #Inject constructor or
from an #Provides-annotated method.
I've read all similar questions on Stackoverflow. I did everything they suggested, but didn't find the answer.
Navigator.class
#Singleton
class Navigator #Inject constructor(private val activity: AppCompatActivity,
private val fragmentManager: FragmentManager)
BaseActivity.class
abstract class BaseActivity: AppCompatActivity(), HasSupportFragmentInjector{
#Inject
lateinit var fragmentInjector: DispatchingAndroidInjector<Fragment>
#Inject
lateinit var navigator: Navigator
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
}
override fun supportFragmentInjector() = fragmentInjector
BaseModule.class
#Module
abstract class BaseActivityModule {
#Module
companion object {
#JvmStatic
#Provides
#Named(ACTIVITY_FRAGMENT_MANAGER)
#PerActivity
fun activityFragmentManager(activity: AppCompatActivity): FragmentManager =
activity.supportFragmentManager
}
#Binds
#PerActivity
abstract fun activity(appCompatActivity: AppCompatActivity): Activity
#Binds
#PerActivity
abstract fun activityContext(activity: Activity): Context
MainActivityModule.class
#Module(includes = [
BaseActivityModule::class
])
abstract class MainActivityModule {
#Binds
#PerActivity
abstract fun appCompatActivity(mainActivity: MainActivity): AppCompatActivity
AppModule.class
#Module(includes = [AndroidSupportInjectionModule::class])
abstract class AppModule {
#Binds
#Singleton
abstract fun application(app: App): Application
#PerActivity
#ContributesAndroidInjector(modules = [MainActivityModule::class])
abstract fun mainActivityInjector(): MainActivity
First, please make sure to understand what the error means. You're trying to inject AppCompatActivity but Dagger complains that it cannot be provided. So far so good. Please always make sure to include the full error as shown in the link, so that we can more easily see what's going on. In your case this seems to be a scoping issue.
You provide a Navigator through constructor injection as a #Singleton...
#Singleton
class Navigator #Inject constructor(private val activity: AppCompatActivity,
private val fragmentManager: FragmentManager)
And you provide/bind AppCompatActivity from a module with #PerActivity scope.
#Binds
#PerActivity
abstract fun appCompatActivity(mainActivity: MainActivity): AppCompatActivity
So unless you have a really crazy setup, #Singleton will not be able to access #PerActivity. If it could, you'd have a memory leak at the very least, since you'd keep your Activity instance around longer than its lifetime (onCreate -> onDestroy)
To solve your problem you have to either move Navigator down in scope to also be #PerActivity and share the lifecycle of the Activities it references (so that it won't leak) or you have to remove the dependency on your Activity in your constructor. If you really do need it to be #Singleton and reference an Activity you might be able to have a setter for the current Activity, but again, watch out for leaks.
I am using Dagger 2.16 and was following this article for my dagger implementation. Everything was working fine with this implementation until I had only one Activity(HomeActivity). As soon as I started implementing Dagger in SplashScreenActivity. I started getting this error. Here is some code from my project
AppComponent.kt
#Singleton
#Component(modules = [
AndroidInjectionModule::class,
AppModule::class,
ActivityBuilder::class,
ServiceBuilder::class,
BroadcastRecieverBuilder::class])
interface AppComponent : AndroidInjector<MyApp> {
#Component.Builder
abstract class Builder : AndroidInjector.Builder<MyApp>()
}
AppModule.kt
#Module()
class AppModule {
#Provides
#Singleton
fun provideContext(application: MyApp): Context {
return application
}
#Provides
#Singleton
fun provideRestService(retrofit: Retrofit): RestService {
return retrofit.create(RestService::class.java)
}
...
}
ActivityBuilder.kt
#Module
abstract class ActivityBuilder {
#ContributesAndroidInjector(modules = [HomeActivityModule::class])
#PerActivity
abstract fun bindHomeActivity(): HomeActivity
#ContributesAndroidInjector(modules = [SplashScreenModule::class])
#PerActivity
abstract fun bindSplashActivity(): SplashScreenActivity
}
BaseActivity.kt
abstract class BaseActivity<V : BaseView, P : MvpBasePresenter<V>> :
MvpActivity<V, P>(), BaseView, HasSupportFragmentInjector {
#Inject
lateinit var fragmentInjector: DispatchingAndroidInjector<Fragment>
#Inject
lateinit var mPresenter: P
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
}
override fun createPresenter(): P = mPresenter
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentInjector
}
}
I have my own BaseActivity instead of DaggerActivity because I what to inherit from mosby's MvpActivity.
SplashScreenModule.kt
#Module
abstract class SplashScreenModule {
#Binds
#PerActivity
internal abstract fun splashPresenter(splashPresenter: SplashScreenPresenter): BasePresenter<*>
}
HomeActivityModule.kt
#Module
abstract class HomeActivityModule {
#Binds
#PerActivity
internal abstract fun homePresenter(homePresenter: HomeActivityPresenter): BasePresenter<*>
#ContributesAndroidInjector(modules = [DownloadFragmentModule::class])
#PerFragment
internal abstract fun downloadsFragment(): DownloadsFragment
}
Now when I build this, I get an error as follows
error: [Dagger/MissingBinding] [dagger.android.AndroidInjector.inject(T)] java.util.Map<java.lang.Class<? extends android.support.v4.app.Fragment>,javax.inject.Provider<dagger.android.AndroidInjector.Factory<? extends android.support.v4.app.Fragment>>> cannot be provided without an #Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.realtime.app.MyApp> {
^
A binding with matching key exists in component: com.realtime.dagger.ActivityBuilder_BindHomeActivity.HomeActivitySubcomponent
java.util.Map<java.lang.Class<? extends android.support.v4.app.Fragment>,javax.inject.Provider<dagger.android.AndroidInjector.Factory<? extends android.support.v4.app.Fragment>>> is injected at
dagger.android.DispatchingAndroidInjector.<init>(injectorFactories)
dagger.android.DispatchingAndroidInjector<android.support.v4.app.Fragment> is injected at
com.realtime.core.BaseActivity.fragmentInjector
com.realtime.splashScreen.SplashScreenActivity is injected at
dagger.android.AndroidInjector.inject(T)
component path: com.realtime.dagger.AppComponent → com.realtime.dagger.ActivityBuilder_BindSplashActivity.SplashScreenActivitySubcomponent
I have gone through other similar que like this but couldn't relate it to what I am facing. What am I missing?
Update: For now I am not inheriting BaseActivity in SplashScreenActivity so that I can avoid injecting fragmentInjector: DispatchingAndroidInjector<Fragment>. It is working for now as I don't have any fragment in SplashScreenActivity.
It works for HomeActivity because it binds a fragment:
#ContributesAndroidInjector
fun downloadsFragment(): DownloadsFragment
SplashScreenActivity does not.
AndroidInjection uses DispatchingAndroidInjector to handle runtime injections, which basically contains a Map of classes to their component builders. This map needs to be injected like everything else. In the case of HomeActivity the fragment declaration in the module generates a binding for the map, which can then be injected.
Since there is no Fragment on the splash activity Dagger does not know about any bindings, let alone any map. Which is why it complains that it cannot be provided.
You can read more here about multibindings.
To prevent this from happening, you should register AndroidInjectionModule on your AppComponent, which just contains the declarations for the empty maps.
While it contains the declaration for android.app.Fragment it does not for android.support.v4.app.Fragment, which is where the error comes from.
So to fix this specific error you should add AndroidSupportInjectionModule to your component, which also includes the support bindings, providing an empty map when there are no fragments in an activity.
#Component(modules = [AndroidSupportInjectionModule::class, /* ... */])
interface AppComponent { /* ... */ }