Architecture-components-samples: [Question] On GithubBrowserSample, how are ViewModel dependencies injected?

Created on 6 Sep 2017  路  17Comments  路  Source: android/architecture-components-samples

Hi,
Thanks for the very useful examples! They were really helpful!

I have two (probably basic) questions regarding Dagger and injection in ViewModels.
Sometimes we want to inject some dependencies in our class constructor. To do that, we just annotate our class constructor with @Inject and the dependencies passed to the constructor will be provided.

In the Architecture Components ViewModel, if we want to pass parameters to a ViewModel constructor, we have to use a Factory that knows how that ViewModel is instantiated. So I assume just annotating the ViewModel with @Inject wont't work, since it is the Factory that is providing the ViewModel instance, not Dagger.

I saw that you do exactly that and use GithubViewModelFactory.java to provide instances of ViewModels.

However, I don't understand two things in this class:
1 - How (T) creator.get() knows how to return a ViewModel instance with its dependencies injected on it?
2 - GithubViewModelFactory.java receives a Map<Class<? extends ViewModel>, Provider<ViewModel>> creators on its constructor. Where is this Map coming from?

Thanks! :)

Most helpful comment

But it does not work for me.I have defined ViewModelKey like below:

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
    Class<? extends ViewModel> value();
}

and ViewModelModule:

@Module
abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(SearchViewModel.class)
    abstract SearchViewModel bindSearchViewModel(SearchViewModel searchViewModel);
}

and AppComponent has include the module:

@Singleton
@Component(modules = {
        ViewModelModule.class,
        AppModule.class,
        AndroidSupportInjectionModule.class,
        ApplicationModule.class,
        ActivityBindingModule.class
})
public interface AppComponent extends AndroidInjector<DaggerApplication> {
    void inject(ViewerApp viewerApp);

    @Override
    void inject(DaggerApplication instance);

    @Component.Builder
    interface Builder {

        @BindsInstance
        AppComponent.Builder application(Application application);

        AppComponent build();
    }
}

But when build the project, i got this in gradle console:

[dagger.android.AndroidInjector.inject(T)] java.util.Map<java.lang.Class<? extends android.arch.lifecycle.ViewModel>,javax.inject.Provider<android.arch.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method.

Is there someone i miss? @florina-muntenescu

All 17 comments

The answer to your questions is in the ViewModelModule file.
The magic is done by Dagger via the @Binds and @IntoMap annotations. A Map<K, Provider<V>> is generated via the @IntoMap annotation and that Map is the constructor param for GithubViewModelFactory

Oh, everything makes sense now!
Thank you very much @florina-muntenescu ! :)

But it does not work for me.I have defined ViewModelKey like below:

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
    Class<? extends ViewModel> value();
}

and ViewModelModule:

@Module
abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(SearchViewModel.class)
    abstract SearchViewModel bindSearchViewModel(SearchViewModel searchViewModel);
}

and AppComponent has include the module:

@Singleton
@Component(modules = {
        ViewModelModule.class,
        AppModule.class,
        AndroidSupportInjectionModule.class,
        ApplicationModule.class,
        ActivityBindingModule.class
})
public interface AppComponent extends AndroidInjector<DaggerApplication> {
    void inject(ViewerApp viewerApp);

    @Override
    void inject(DaggerApplication instance);

    @Component.Builder
    interface Builder {

        @BindsInstance
        AppComponent.Builder application(Application application);

        AppComponent build();
    }
}

But when build the project, i got this in gradle console:

[dagger.android.AndroidInjector.inject(T)] java.util.Map<java.lang.Class<? extends android.arch.lifecycle.ViewModel>,javax.inject.Provider<android.arch.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method.

Is there someone i miss? @florina-muntenescu

maybe you should return ViewModel , not the SearchViewModel

What to do when in MyViewModel need to inject something with scope from some subcomponent?

@lexgorbunov

I also have the same problem if MyCustomViewModel that provided by App scope depends on other object that provided by user scope.

Just move MyCustomViewModel to user scope to fix it.
If someone has better answer, @me please!

@florina-muntenescu Why you have to override the in ViewModelFactory? What is the purpose GithubViewModelFactory? What is wrong with the default implementation of ViewModelFactory?

Leaving a note in case anyone else has the same issue I did when following this sample using kotlin. If you get this error (same as https://github.com/googlesamples/android-architecture-components/issues/141#issuecomment-344869162)

java.util.Map<java.lang.Class<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method.

You'll need to add a @JvmSuppressWildcards annotation to the creator map's value type param:

@Singleton
class ViewModelFactory
@Inject constructor(
        private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
    ...
}

How would you fix this for java? I don't think @JvmSuppressWildcards is part of the java library? Any thoughts?

Switching from Map to MutableMap in kotlin seems to fix the issue for me. @JvmSuppressWildcards seems to work as well

@LeaYw you need to bind the factory also, like this -

@Binds
@Your scope (
abstract ViewModelProvider.Factory bind<yourViewModelFactory>(<yourViewModelFactory> factory);

@lexgorbunov

I also have the same problem if MyCustomViewModel that provided by App scope depends on other object that provided by user scope.

Just move MyCustomViewModel to user scope to fix it.
If someone has better answer, @me please!

Yes, this is the first thing need to check.

Leaving a note in case anyone else has the same issue I did when following this sample using kotlin. If you get this error (same as #141 (comment))

java.util.Map<java.lang.Class<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method.

You'll need to add a @JvmSuppressWildcards annotation to the creator map's value type param:

@Singleton
class ViewModelFactory
@Inject constructor(
        private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
    ...
}

I had this problem, thank you for sharing it

I had this problem

java.util.Map,? extends javax.inject.Provider> cannot be provided without an @Provides-annotated method.

in my case , i was updated my Kotlin version to 1.3.30 !
i have just changed it to 1.3.21 and problem has been fixed.

classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.21"

in my case , i was updated my Kotlin version to 1.3.30 !
i have just changed it to 1.3.21 and problem has been fixed.

Your case is another issue though from https://youtrack.jetbrains.com/issue/KT-30979

in my case , i was updated my Kotlin version to 1.3.30 !
i have just changed it to 1.3.21 and problem has been fixed.

Your case is another issue though from https://youtrack.jetbrains.com/issue/KT-30979

Thank you so much :+1:
I just lost 2 days on this that I'll never get back. I guess that's the price of being an early adopter.

I truly haven't a clue what i'm doing wrong, my instance of my ViewModelFactory is always null on my fragment so i presume it isn't injecting properly, i do a debug on this sample and it goes into it fine, but not mine.

Not a clue why not, as far as I can see other than some basic name changes, it's the same thing! I even used the same structure to ensure it is not anything stupid. Initially started with the sunflower app and slowly been redesigning to work with this as i realised sunflower app doesn't use Rest at all.

@starkej2 thanks for that fix, but can anyone explain why it is needed or how it fixes the issue? @florina-muntenescu ?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

amunozg picture amunozg  路  3Comments

prabinshrestha picture prabinshrestha  路  3Comments

ntyrrell-decodedsolutions picture ntyrrell-decodedsolutions  路  3Comments

george5613 picture george5613  路  4Comments

prabinshrestha picture prabinshrestha  路  5Comments