4

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.

Rupesh
  • 3,415
  • 2
  • 26
  • 30
  • Maybe you should make the ViewModel @Singleton? And if take a look at your ViewModelFactory (you did not showed it, but I suppose it as typical implementation) you will see, that factory is only can create viewmodels when you touch it. – redlabrat Jul 26 '18 at 09:02
  • You want to inject the ViewModelProviders, and not the ViewModel itself. Please see a good example there : https://proandroiddev.com/viewmodel-with-dagger2-architecture-components-2e06f06c9455 – NSimon Jul 26 '18 at 09:08
  • Factory has typical implementation. I don't think making MainViewModel Singleton is good idea as ViewModel objects are automatically retained during configuration changes. – Rupesh Jul 26 '18 at 09:11
  • The idea of ViewModelsProviders is to store the instances of ViewModels between configuration changes in ViewModelStores. I think that your code works like this: when activity is destroyed due to configuration changes MainModule is destroyed too, and after new instance of MainActivity is created dagger will create new MainModule for it and that is why you get new instance of ViewModelProvider and new instance of ViewModel. – redlabrat Jul 26 '18 at 09:13
  • @redlabrat was thinking in the same direction but module is stateless here, you can see all the methods are static.. besides if I inject factory and use that factory to create ViewModel I get the old instance after rotation.. how is that working? – Rupesh Jul 26 '18 at 09:21
  • As I sad factory is not doing anything, factory only provides an instance of ViewModel and in that case (when you provide factory) your ViewModelProvider is stayed the same and provider just takes saved instance of your ViewModel from ViewModelStore. – redlabrat Jul 26 '18 at 09:25
  • Take a look on this question: https://stackoverflow.com/questions/37848563/dagger-2-lifecycle-of-a-component-module-and-scope – redlabrat Jul 26 '18 at 09:29
  • hmm.. seems convincing.. thanks.. ll dig up more.. – Rupesh Jul 26 '18 at 09:30
  • @redlabrat the issue was related to AndroidInjection and the state saving.. please have a look at my answer.. thanks..!! – Rupesh Jul 26 '18 at 13:29

1 Answers1

14

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.

Rupesh
  • 3,415
  • 2
  • 26
  • 30