hushed_voice
hushed_voice

Reputation: 3608

How to use Dagger 2 to Inject ViewModel of same Fragments inside ViewPager

I am trying to add Dagger 2 to my project. I was able to inject ViewModels (AndroidX Architecture component) for my fragments.

I have a ViewPager which has 2 instances of the same fragment (Only a minor change for each tabs) and in each tab, I am observing a LiveData to get updated on data change (from API).

The issue is that when the api response comes and updates the LiveData, the same data in the currently visible fragment is being sent to observers in all the tabs. (I think this is probably because of the scope of the ViewModel).

This is how I am observing my data:

override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        activityViewModel.expenseList.observe(this, Observer {
            swipeToRefreshLayout.isRefreshing = false
            viewAdapter.setData(it)
        })
    ....
}

I am using this class for providing ViewModels:

class ViewModelProviderFactory @Inject constructor(creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>?) :
    ViewModelProvider.Factory {
    private val creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>? = creators
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        var creator: Provider<out ViewModel?>? = creators!![modelClass]
        if (creator == null) { // if the viewmodel has not been created
// loop through the allowable keys (aka allowed classes with the @ViewModelKey)
            for (entry in creators.entries) { // if it's allowed, set the Provider<ViewModel>
                if (modelClass.isAssignableFrom(entry.key!!)) {
                    creator = entry.value
                    break
                }
            }
        }
        // if this is not one of the allowed keys, throw exception
        requireNotNull(creator) { "unknown model class $modelClass" }
        // return the Provider
        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }

    companion object {
        private val TAG: String? = "ViewModelProviderFactor"
    }
}

I am binding my ViewModel like this:

@Module
abstract class ActivityViewModelModule {
    @MainScope
    @Binds
    @IntoMap
    @ViewModelKey(ActivityViewModel::class)
    abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}

I am using @ContributesAndroidInjector for my fragment like this:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

And I am adding these modules to my MainActivity subcomponent like this:

@Module
abstract class ActivityBuilderModule {
...
    @ContributesAndroidInjector(
        modules = [MainViewModelModule::class, ActivityViewModelModule::class,
            AuthModule::class, MainFragmentBuildersModule::class]
    )
    abstract fun contributeMainActivity(): MainActivity
}

Here is my AppComponent:

@Singleton
@Component(
    modules =
    [AndroidSupportInjectionModule::class,
        ActivityBuilderModule::class,
        ViewModelFactoryModule::class,
        AppModule::class]
)
interface AppComponent : AndroidInjector<SpenmoApplication> {

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun application(application: Application): Builder

        fun build(): AppComponent
    }
}

I am extending DaggerFragment and injecting ViewModelProviderFactory like this:

@Inject
lateinit var viewModelFactory: ViewModelProviderFactory

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
....
activityViewModel =
            ViewModelProviders.of(this, viewModelFactory).get(key, ActivityViewModel::class.java)
        activityViewModel.restartFetch(hasReceipt)
}

the key will be different for both the fragments.

How can I make sure that only the observer of the current fragment is getting updated.

EDIT 1 ->

I have added a sample project with the error. Seems like the issue is happening only when a custom scope is added. Please check out the sample project here: Github link

master branch has the app with the issue. If you refresh any tab (swipe to refresh) the updated value is getting reflected in both the tabs. This is only happening when I add a custom scope to it (@MainScope).

working_fine branch has the same app with no custom scope and its working fine.

Please let me know if the question is not clear.

Upvotes: 7

Views: 2270

Answers (2)

azizbekian
azizbekian

Reputation: 62189

I want to recap the original question, here's it:

I am currently using the working fine_branch, but I want to know, why would using scope break this.

As per my understanding your have an impression, that just because you are trying to obtain an instance of ViewModel using different keys, then you should be provided different instances of ViewModel:

// in first fragment
ViewModelProvider(...).get("true", PagerItemViewModel::class.java)

// in second fragment
ViewModelProvider(...).get("false", PagerItemViewModel::class.java)

The reality, is a bit different. If you put following log in fragment you'll see that those two fragments are using the exact same instance of PagerItemViewModel:

Log.i("vvv", "${if (oneOrTwo) "one:" else "two:"} viewModel hash is ${viewModel.hashCode()}")

Let's dive in and understand why this happens.

Internally ViewModelProvider#get() will try to obtain an instance of PagerItemViewModel from a ViewModelStore which is basically a map of String to ViewModel.

When FirstFragment asks for an instance of PagerItemViewModel the map is empty, hence mFactory.create(modelClass) is executed, which ends up in ViewModelProviderFactory. creator.get() ends up calling DoubleCheck with following code:

  public T get() {
    Object result = instance;
    if (result == UNINITIALIZED) { // 1
      synchronized (this) {
        result = instance;
        if (result == UNINITIALIZED) {
          result = provider.get();
          instance = reentrantCheck(instance, result); // 2
          /* Null out the reference to the provider. We are never going to need it again, so we
           * can make it eligible for GC. */
          provider = null;
        }
      }
    }
    return (T) result;
  }

The instance is now null, hence a new instance of PagerItemViewModel is created and is saved in instance (see // 2).

Now the exact same procedure happens for SecondFragment:

  • fragment asks for an instance of PagerItemViewModel
  • map now is not empty, but does not contain an instance of PagerItemViewModel with key false
  • a new instance of PagerItemViewModel is initiated to be created via mFactory.create(modelClass)
  • Inside ViewModelProviderFactory execution reaches creator.get() whose implementation is DoubleCheck

Now, the key moment. This DoubleCheck is the same instance of DoubleCheck that was used for creating ViewModel instance when FirstFragment asked for it. Why is it the same instance? Because you've applied a scope to the provider method.

The if (result == UNINITIALIZED) (// 1) is evaluating to false and the exact same instance of ViewModel is being returned to the caller - SecondFragment.

Now, both fragments are using the same instance of ViewModel hence it is perfectly fine that they are displaying the same data.

Upvotes: 2

Vishal Arora
Vishal Arora

Reputation: 2564

Both the fragments receive the update from livedata because viewpager keeps both the fragments in resumed state. Since you require the update only on the current fragment visible in the viewpager, the context of the current fragment is defined by the host activity, the activity should explicitly direct updates to the desired fragment.

You need to maintain a map of Fragment to LiveData containing entries for all the fragments(make sure to have an identifier that can differentiate two fragment instances of the same fragment) added to viewpager.

Now the activity will have a MediatorLiveData observing the original livedata observed by the fragments directly. Whenever the original livedata posts an update, it will be delivered to mediatorLivedata and the mediatorlivedata in turen will only post the value to livedata of the current selected fragment. This livedata will be retrieved from the map above.

Code impl would look like -

class Activity {
    val mapOfFragmentToLiveData<FragmentId, MutableLiveData> = mutableMapOf<>()

    val mediatorLiveData : MediatorLiveData<OriginalData> = object : MediatorLiveData() {
        override fun onChanged(newData : OriginalData) {
           // here get the livedata observed by the  currently selected fragment
           val currentSelectedFragmentLiveData = mapOfFragmentToLiveData.get(viewpager.getSelectedItem())
          // now post the update on this livedata
           currentSelectedFragmentLiveData.value = newData
        }
    }

  fun getOriginalLiveData(fragment : YourFragment) : LiveData<OriginalData> {
     return mapOfFragmentToLiveData.get(fragment) ?: MutableLiveData<OriginalData>().run {
       mapOfFragmentToLiveData.put(fragment, this)
  }
} 

class YourFragment {
    override fun onActivityCreated(bundle : Bundle){
       //get activity and request a livedata 
       getActivity().getOriginalLiveData(this).observe(this, Observer { _newData ->
           // observe here 
})
    }
}

Upvotes: 0

Related Questions