rkr87
rkr87

Reputation: 181

Unable to get Dagger to Inject Viewmodel

I've been following a guide to setup DI in an Android application and as far as I can tell I have everything setup correctly. However, I'm getting the following error:

java.lang.RuntimeException: Cannot create an instance of class com.topper.topper.ui.viewmodel.ProfileViewModel

Below are cut-down versions (for brevity) of my classes:

ActivityModule

@Module
public abstract class ActivityModule {
    @ContributesAndroidInjector(modules = FragmentModule.class)
    abstract MainActivity contributeMainActivity();
}

FragmentModule

@Module
public abstract class FragmentModule {
    @ContributesAndroidInjector
    abstract ProfileFragment contributeProfileFragment();
}

ViewModelModule

@Module
public abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(ProfileViewModel.class)
    abstract ViewModel bindProfileViewModel(ProfileViewModel profileViewModel);

    @Binds
    abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory factory);
}

AppModule

@Module(includes = ViewModelModule.class)
public class AppModule {

    @Provides
    @Singleton
    TopperDB provideDatabase(Application application) {
        return Room.databaseBuilder(application,
                TopperDB.class, "TopperDB.db")
                .build();
    }

    @Provides
    @Singleton
    CachedImageDao provideCachedImageDao(TopperDB database) {
        return database.cachedImageDao();
    }

    @Provides
    @Singleton
    Executor provideExecutor() {
        return Executors.newSingleThreadExecutor();
    }

    @Provides
    @Singleton
    CachedImageRepository provideCachedImageRepository(CachedImageDao cachedImageDao, Executor executor) {
        return new CachedImageRepository(cachedImageDao, executor);
    }
}

AppComponent

@Singleton
@Component(modules = {AndroidSupportInjectionModule.class, AndroidInjectionModule.class, ActivityModule.class, FragmentModule.class, AppModule.class})
public interface AppComponent {

    void inject(TopperApp app);

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(Application application);

        AppComponent build();
    }
}

ViewModelKey

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

ViewModelFactory

@Singleton
public class ViewModelFactory implements ViewModelProvider.Factory {

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

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

    @NotNull
    @SuppressWarnings("unchecked")
    @Override
    public <T extends ViewModel> T create(@NotNull 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);
        }
    }
}

CachedImageRepository

@Singleton
public class CachedImageRepository extends BaseRepository {

    private final CachedImageDao cachedImageDao;
    private final Executor executor;

    @Inject
    public CachedImageRepository(CachedImageDao cachedImageDao, Executor executor) {
        this.cachedImageDao = cachedImageDao;
        this.executor = executor;
    }
}

ProfileViewModel

public class ProfileViewModel extends ViewModel {

    private CachedImageRepository cachedImageRepo;

    @Inject
    public ProfileViewModel(CachedImageRepository cachedImageRepo) {
        this.cachedImageRepo = cachedImageRepo;
    }
}

ProfileFragment

public class ProfileFragment extends BaseFragment {

    @Inject
    ViewModelProvider.Factory viewModelFactory;

    private ProfileViewModel mViewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        configureDagger();
    }

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {

        FragmentProfileBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_profile, container, false);

        mViewModel = ViewModelProviders.of(this.requireActivity(), viewModelFactory).get(ProfileViewModel.class);
        mViewModel.init();
        binding.setProfileViewModel(mViewModel);

        return binding.getRoot();
    }

    private void configureDagger() {
        AndroidSupportInjection.inject(this);
    }

}

MainActivity

public class MainActivity extends AppCompatActivity implements ProgressDisplay, HasSupportFragmentInjector {

    @Inject
    DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;

    @Inject
    ViewModelProvider.Factory viewModelFactory;

    private AppBarLayout appBarLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.configureDagger();
    }

    private void configureDagger() {
        AndroidInjection.inject(this);
    }

}

TopperApp

public class TopperApp extends Application implements HasActivityInjector {

    public Context ctx;
    @Inject
    DispatchingAndroidInjector<Activity> dispatchingAndroidInjector;

    @Override
    public void onCreate() {
        super.onCreate();
        this.initDagger();
        ctx = getApplicationContext();
    }

    public Context getAppContext() {
        return ctx;
    }

    @Override
    public DispatchingAndroidInjector<Activity> activityInjector() {
        return dispatchingAndroidInjector;
    }

I think I've included all of the relevant detail above but if I've missed anything please let me know.

Any help would be greatly appreciated, I've been bashing my head against the wall for a few days with this.

Thanks.

EDIT: If it helps, I've tried adding an empty constructor to ProfileViewModel which results in the following error:

java.lang.NullPointerException: Attempt to invoke virtual method 'void com.topper.topper.data.repo.CachedImageRepository.cacheImage(android.content.Context, java.lang.String, int)' on a null object reference

So it appears dagger isn't injecting into the constructor for ProfileViewModel.

Upvotes: 1

Views: 881

Answers (2)

rkr87
rkr87

Reputation: 181

Turn out the issue was in my Fragment class.

Changed from;

public class ProfileFragment extends BaseFragment {

    @Inject
    ViewModelProvider.Factory viewModelFactory;

    private ProfileViewModel mViewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        configureDagger();
    }

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {

        FragmentProfileBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_profile, container, false);

        mViewModel = ViewModelProviders.of(this.requireActivity(), viewModelFactory).get(ProfileViewModel.class);
        mViewModel.init();
        binding.setProfileViewModel(mViewModel);

        return binding.getRoot();
    }

    private void configureDagger() {
        AndroidSupportInjection.inject(this);
    }

}

to

public class ProfileFragment extends BaseFragment {

    @Inject
    ViewModelProvider.Factory viewModelFactory;

    private ProfileViewModel mViewModel;

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {

        configureDagger();

        FragmentProfileBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_profile, container, false);

        mViewModel = ViewModelProviders.of(this.requireActivity(), viewModelFactory).get(ProfileViewModel.class);
        mViewModel.init();
        binding.setProfileViewModel(mViewModel);

        return binding.getRoot();
    }

    private void configureDagger() {
        AndroidSupportInjection.inject(this);
    }

}

Upvotes: 3

Vishal Arora
Vishal Arora

Reputation: 2564

Dagger can't create ViewModel on its own. ViewModel instantiation is done through an instance of ViewModelProvider.Factory. You need to tell dagger to how it can create an instance of ProfileViewModel.

So @Binds in this case won't work for you. You need to define a method which returns an instance of ProfileViewModel and annotate it with @Provides.

For instance -

@Module
public class ViewModelModule {

@Provides
@IntoMap
@ViewModelKey(ProfileViewModel.class)
public ProfileViewModel bindProfileViewModel(ViewModelFactory factory) {
    return factory.create();
}

@Provides
public ViewModelProvider.Factory bindViewModelFactory(){
    return new ViewModelFactory();
}

Refer to this for more detail -

Why a viewmodel factory is needed in Android?

Upvotes: 0

Related Questions