Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewModel is recreated after rotation; if injected directly with dagger2

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.

like image 557
Rupesh Avatar asked Dec 17 '22 21:12

Rupesh


1 Answers

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.

like image 188
Rupesh Avatar answered Dec 28 '22 07:12

Rupesh