0

So i am new to Dagger 2 dependency injection. I have created a custom ViewModelFactory class which returns my ViewModel.

@Singleton
public class CustomViewModelFactory implements ViewModelProvider.Factory {
    private final MyCatchesRepository repository;

    @Inject
    public CustomViewModelFactory(MyCatchesRepository repository) {
        this.repository = repository;
    }

    @NonNull
    @Override
    @SuppressWarnings("unchecked")
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        if (modelClass.isAssignableFrom(MyCatchViewModel.class)) {
            return (T) new MyCatchViewModel(repository);
        } else {
            throw new IllegalArgumentException("ViewModel Not Found");
        }
    }
}

The CustomViewModel takes a MyCatchesRepository in the constructor and then creates the MyCatchViewModel. How could i change this class so that i can use this ViewModelFactory to create different ViewModels with different constructor arguments (repositories)

This is the Module where the CustomViewModelFactory is created

@Module
public class RoomModule {

    private final MyDatabase myDatabase;

    public RoomModule(Application application) {
        this.myDatabase = Room.databaseBuilder(application,
                            MyDatabase.class, AppConstants.DATABASE_NAME)
                            .build();
    }

    @Provides
    @Singleton
    MyCatchesRepository provideCatchesRepository(MyCatchDao myCatchDao) {
        return new MyCatchesRepository(myCatchDao);
    }

    @Provides
    @Singleton
    MyCatchDao providesCatchDao(MyDatabase myDatabase) {
        return myDatabase.myCatchDao();
    }


    @Provides
    @Singleton
    LuresRepository provideLureRepository(LureDao lureDao) {
        return new LuresRepository(lureDao);
    }

    @Provides
    @Singleton
    LureDao provideLureDao(MyDatabase myDatabase) {
        return myDatabase.lureDao();
    }

    @Provides
    @Singleton
    MyDatabase provideDatabase(Application application) {
        return myDatabase;
    }

    @Provides
    @Singleton
    ViewModelProvider.Factory provideCatchesViewModelFactory(MyCatchesRepository catchesRepository) {
        return new CustomViewModelFactory(catchesRepository);
    }
}

ViewModelModule

@Module
public abstract class ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(MyCatchViewModel.class)
    abstract ViewModel myCatchViewModel(MyCatchViewModel myCatchViewModel);

    @Binds
    @IntoMap
    @ViewModelKey(FishingSpotViewModel.class)
    abstract ViewModel fishingSpotViewModel(FishingSpotViewModel fishingSpotViewModel);

    @Binds
    abstract ViewModelProvider.Factory bindCustomViewModelFactory(CustomViewModelFactory customViewModelFactory);

}
pavlos
  • 547
  • 1
  • 9
  • 23

1 Answers1

1

The approach the Google team came up with in the architecture components samples is to use a custom annotation in order to provide ViewModel classes through dagger.

In Java the Annotation looks as follows.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import androidx.lifecycle.ViewModel;
import dagger.MapKey;

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

This uses MapKey from Dagger, where any annotated ViewModel will be composed into a Map which can then be used in your ViewModelFactory.

In the Google samples the ViewModelFactory looks as follows, where using constructor injection you can access the map of ViewModel providers.

public class ViewModelFactory implements ViewModelProvider.Factory {

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

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

    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        Provider<ViewModel> viewModelProvider = viewModels.get(modelClass);

        if (viewModelProvider == null) {
            throw new IllegalArgumentException("model class " + modelClass + " not found");
        }

        //noinspection unchecked
        return (T) viewModelProvider.get();
    }
}

In your example your would end up with the following in order to provide the MyCatchViewModel. Other ViewModels could then be provided by following the same pattern.

@Module
public abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(MyCatchViewModel.class)
    abstract ViewModel myCatchViewModel(MyCatchViewModel myCatchViewModel);
}

For a complete example you can check out the GithubBrowserSample sample from Google. https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelModule.kt

Chris
  • 2,332
  • 1
  • 14
  • 17
  • I have gone through the code provided and the sample from Google. I only manage to get the MyCatchViewModel in the MyCatchFragment. When i try to get the FishingSpotViewModel it trows an exception `java.lang.RuntimeException: Cannot create an instance of class com.applab.fishmemommvm.viewmodel.FishingSpotViewModel` – pavlos Sep 17 '18 at 12:35
  • I am going to link the ViewModelModule class above – pavlos Sep 17 '18 at 12:36
  • So MyCatchViewModel works? Do you have all your dependencies setup for FishingSpotViewModel? Do you also have an Inject setup on your ViewModel constructors, e.g. https://github.com/googlesamples/android-architecture-components/blob/b1a194c1ae267258cd002e2e1c102df7180be473/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java#L47 – Chris Sep 17 '18 at 12:53
  • yea that what the problem. I missed an Inject setup on one of my ViewModels. Thank you very much for the help and guidance! :) – pavlos Sep 17 '18 at 13:20