Reputation: 181
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
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
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