1

I'm just starting with Dependency Injection using Dagger Framework, so it might be I'm missing something trivial, but I've spent more than 2 days just going over the code and the tutorial.

So, I am trying to use dagger dependency injection for my ViewModels, I found that I could do that with a feature called multi binding, which I've implemented. I have an AppComponent and an ActivitySubComponent. The AppComponent installs a ViewModelFactoryModule which @Binds a ViewModelProvider.Factory, I want this Module in the Application because I think it should be independent of the Activity life-cycle. And in my ActivitySubComponent I have a ViewModelsModule which @Binds a ViewModel.

Now per my understanding I have included ActivitySubComponent in the ViewModelFactoryModule which will add the subcomponent to the AppComponent.

Edit :

Now when I make/build the project I get the following error. Apologies, the original error was:

error: [Dagger/MissingBinding] java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method.
java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> is injected at
com.mobile.presentation.viewmodels.build.ViewModelFactory(creators)
com.mobile.presentation.viewmodels.build.ViewModelFactory is provided at
com.mobile.app.AppComponent.obtainViewModelsFactory()

The following error was produced after adding following method to AppComponent.java Map<Class<? extends ViewModel>, Provider<ViewModel>> obtainMap();.

error: [Dagger/MissingBinding] java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method.
java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> is provided at
com.mobile.app.AppComponent.obtainMap()
It is also requested at:
com.mobile.presentation.viewmodels.build.ViewModelFactory(creators)
The following other entry points also depend on it:
com.mobile.app.AppComponent.obtainViewModelsFactory()

Enough jibber jabber, here is the relevant code:

AppComponent.java

@AppScope
@Component(
        modules = {
                AppModule.class,
                ViewModelFactoryModule.class
        })
public interface AppComponent {
    App obtainApp();

    ViewModelFactory obtainViewModelsFactory();

    // I tried with adding the following line, out of sheer annoyance.
    // Map<Class<? extends ViewModel>, Provider<ViewModel>> obtainMap();
}

ViewModelFactoryModule.java

@Module(subcomponents = ActivitySubComponent.class)
public abstract class ViewModelFactoryModule {
    @AppScope
    @Binds
    public abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory viewModelFactory);
}

ViewModelsFactory.java

@AppScope
public class ViewModelFactory implements ViewModelProvider.Factory {

    private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;

    @Inject
    public ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
        this.creators = creators;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass)
    {
        Provider<? extends ViewModel> creator = creators.get(modelClass);
        if (creator == null) {
            for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
                if (modelClass.isAssignableFrom(entry.getKey())) {
                    creator = entry.getValue();
                    break;
                }
            }
        }

        if (creator == null) {
            throw new IllegalArgumentException("unknown model class " + modelClass);
        }

        try {
            return (T) creator.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

ActivitySubComponent.java

// I have a separate module for my `LiveData`.
@ActivityScope
@Subcomponent(
        modules = {
                ActivityModule.class,
                ViewModelsModule.class,
                SplashLiveDataModule.class
        }
)
public interface ActivitySubComponent {

    Activity obtainActivity();

    LiveData<InitialAPIResponse> obtainLiveInitialApiResponse();

    LiveData<InitialSplashApiStatus> obtainLiveInitialSplashApiStatus();

    @Subcomponent.Builder
    interface Builder {
        ActivitySubComponent build();

        Builder setActivityModule(ActivityModule activityModule);
    }
}

ViewModelsModule.java

@Module
public abstract class ViewModelsModule {

    @ActivityScope
    @Binds
    @IntoMap
    @ViewModelKey(SplashViewModel.class)
    public abstract ViewModel provideVideoListViewModel(SplashViewModel viewModel);
}

Now, I understand from this that the AppComponent cannot have direct access to ActivitySubComponent, but it should be able to access the SubComponent since a @Subcomponent is generated as an inner class of the component (perhaps I'm missing something), and I don't think ViewModels should be bound to the application's component (I say this because if I move my ViewModelsModule to the AppComponent too I can run the code and everything works right), am I wrong in my logical approach?

dagger dependencies:

implementation 'com.google.dagger:dagger:2.22'
annotationProcessor 'com.google.dagger:dagger-compiler:2.22'
Abbas
  • 3,529
  • 5
  • 36
  • 64
  • Kindly post ViewModelFactory code here. – Arslan Shoukat Sep 16 '19 at 10:24
  • @ArslanShoukat added. – Abbas Sep 16 '19 at 10:27
  • 1
    Follow the GithubBrowserSample example from google: https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample/app/src/main/java/com/android/example/github/di This is a pretty simple and scalable setup – denvercoder9 Sep 16 '19 at 11:34
  • @sonnet If I've got my Kotlin right, I see from the example, that all the dependencies around `Viewmodel` and `Factory` are put together into one `AppComponent`. If that is true, this approach would force me to add all my `ViewModel`s into the `AppComponent`. After a while things will start to clutter up. Is there no other solution to avoid that clutter? – Abbas Sep 16 '19 at 11:51
  • It won't become a clutter because `AppComponent` only lists the `@Module`s you have in your project. You don't have to list every module there. For example, the `ViewModelModule` is referenced in the `AppModule`: `@Module(includes = [ViewModelModule::class]) class AppModule { .. }`. If you have a multi-module architecture, you could have several viewmodelmodules specific to each of the module (IDE module) and reference all of them in the `app`'s `ViewModelModule` or the downstream feature that depends on these modules. I found this way to be very maintainable for mid-large projects (~100k LOC) – denvercoder9 Sep 16 '19 at 12:04
  • @sonnet Hmmm... interesting, yes adding other Module dependencies to other Modules instead of a Component would be nice. – Abbas Sep 16 '19 at 12:14
  • @sonnet one more thing however and this maybe a bit out of the scope for the question, how would you then handle communication between Use Case and ViewModel in a clean architecture? Earlier I could create a contract interface, but now since the `UseCase` is a dependency of `ViewModel` I can't set a callback from `UseCase` to `ViewModel`, lest it breaks the dependency graph. – Abbas Sep 16 '19 at 12:19
  • I've thought about using a method injector on the Interactor, but I'm not sure that is correct approach. – Abbas Sep 16 '19 at 12:20
  • 1
    Instead of passing/injecting a callback into the `UseCase`, I expose/return observables from `UseCase`s. Here's a basic sample with usecases, repository pattern and coroutines: https://github.com/googlesamples/android-architecture/tree/usecases – denvercoder9 Sep 16 '19 at 12:28
  • 1
    Here's the boilerplatey way of doing components and subcomponents without dagger-android (since you want to delve into dagger more) https://stackoverflow.com/a/29943394/2235972 And here's a simple way of setting up di with dagger & dagger-android https://stackoverflow.com/a/58009976/2235972 – denvercoder9 Sep 22 '19 at 10:30
  • @Abbas can find a solution? – ysfcyln Mar 22 '20 at 13:21

1 Answers1

1

You are right. Adding ActivitySubComponent in the ViewModelFactoryModule will add the subcomponent to the AppComponent.

When you want to add subcomponent to parent, you have two way of doing. First by adding subcomponent in a module and then adding that module to parent. Second, by declaring an abstract method in parent component that returns subcomponent.

You got first part right but in your AppComponent, you have created a method that returns ViewModelFactory which is already provided by ViewModelFactoryModule. This conflicts with object graph and due to that your build fails. You need to remove both methods that return ViewModelFactory and Map and this will fix the problem.

Arslan Shoukat
  • 432
  • 4
  • 10
  • Rather than creating sub-components yourself and writing lots of boilderplate code, you should start using dagger-android dependencies that facilitate in injecting dependencies for android. Try this course(https://codingwithmitch.com/courses/dagger22-android/) from Mitch. This will help in understanding how di in android can be achieved with minimal coding. – Arslan Shoukat Sep 16 '19 at 11:31
  • Hmm... I understand why a Component cannot depend on a `Subcomponent`. However, this still doesn't answer the question, how do you then split the the job between components? ViewModelFactory clearly has to be bound with `AppComponent` otherwise it will create new factory and in turn new `ViewModel`s in each `Activity` whenever the `ActivityComponent` is initiated. – Abbas Sep 16 '19 at 11:37
  • 1
    I see from your comment, the link for Dagger-Android. But as stated in the question I'm learning how to use Dagger and would prefer to have dived into the boiler-plate code at least once, just to have an understanding of how things should work. – Abbas Sep 16 '19 at 11:39
  • GoogleSample (https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample/app/src/main/java/com/android/example/github/di) has a sample app with dependency injection using dagger-android. You can use this in your app. – Arslan Shoukat Sep 16 '19 at 11:39