Possible duplicate of this
I am exploring android injections api with dagger2. So, in my sample application I have injected ViewModel
directly in the activity; have a look at following code snippets.
class SampleApp : Application(), HasActivityInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
override fun activityInjector(): AndroidInjector<Activity> =
dispatchingAndroidInjector
override fun onCreate() {
super.onCreate()
DaggerApplicationComponent.builder()
.application(this)
.build()
.inject(this)
}
}
@Component(modules = [
AndroidInjectionModule::class,
ActivityBindingModule::class,
AppModule::class
/** Other modules **/
])
@Singleton
interface ApplicationComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun application(application: Application): Builder
fun build(): ApplicationComponent
}
fun inject(sampleApp: SampleApp)
}
@Module
public abstract class ActivityBindingModule {
@ContributesAndroidInjector(modules = MainModule.class)
public abstract MainActivity contributeMainActivityInjector();
}
class MainActivity : AppCompatActivity() {
@Inject
lateinit var mainViewModel: mainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dashboard)
}
}
@Module
public class MainModule {
@Provides
public static MainViewModelProviderFactory provideMainViewModelProviderFactory(/** some dependencies **/) {
return new MainViewModelProviderFactory(/** some dependencies **/);
}
@Provides
public static MainViewModel provideMainViewModel(MainActivity activity, MainViewModelProviderFactory factory) {
return ViewModelProviders.of(activity, factory).get(MainViewModel.class);
}
}
as you can see I have injected MainViewModel
directly into the activity. Now, if I rotate the activity the instance being injected is different.
But, if I inject the MainViewModelProviderFactory
in the MainActivity
and perform
ViewModelProviders.of(activity, factory).get(MainViewModel.class)
it returns the same instance as before.
I'm not getting what is wrong with my implementation.
Any pointers would be appreciable.
So after going through the source of ViewModelProvider
, ViewModelProviders
, FragmentActivity
and yes the dagger2 documentation
I have an answer..
Feel free to correct me if I'm wrong..
We must not inject ViewModel directly, we should inject factories instead.
I am facing this issue due to this line AndroidInjection.inject(this)
.
As per the dagger authors
It is crucial to call AndroidInjection.inject() before super.onCreate() in an Activity
Let's see what is going wrong here at very high level..
Activity will retain it's ViewModel
on rotation using onRetainNonConfigurationInstance
and will restore it in onCreate()
As we are injecting before the call to super.onCreate()
, we will not get the retained MainViewModel
object but the new one.
If you want details, read on..
When dagger tries to inject MainViewModel
it calls provideMainViewModel()
method of MainModule
, which invokes following expression (keep in mind super.onCreate()
is not yet called)
ViewModelProviders.of(activity, factory).get(MainViewModel.class)
The ViewModelProviders.of
will return a ViewModelProvider
which holds the references for ViewModelStore
of respective activity and ViewModelProviderFactory
public static ViewModelProvider of(@NonNull FragmentActivity activity,
@Nullable Factory factory) {
.
.
return new ViewModelProvider(ViewModelStores.of(activity), factory);
}
ViewModelStore.of(activity)
will ultimately give call to activity's getViewModelStore()
as the activity in this case is AppCompatActivity
which implements ViewModelStoreOwner
AppCompatActivity
creates new ViewModelStore
if it is null & holds a reference to it. ViewModelStore
is a wrapper over Map<String, ViewModel>
with additional method clear()
@NonNull
public ViewModelStore getViewModelStore() {
if (this.getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the Application instance. You can't request ViewModel before onCreate call.");
} else {
if (this.mViewModelStore == null) {
this.mViewModelStore = new ViewModelStore();
}
return this.mViewModelStore;
}
}
Whenever the device gets rotated activity retains it's non configuration instance state using onRetainNonConfigurationInstance
and restores it in onCreate
. (e.g. mViewModelStore)
ViewModelProvider.get
will try to fetch the ViewModel
from activity's ViewModelStore
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
//noinspection unchecked
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
//noinspection unchecked
return (T) viewModel;
}
In this particular example; as we haven't called super.onCreate()
method yet the implementation will ask factory
to create it and will update the corresponding ViewModelStore
.
And hence we ended up having two different MainViewModel
objects.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With