如何使用 Dagger 2 在 ViewPager 中注入相同片段的 ViewModel

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

我正在尝试将 Dagger 2 添加到我的项目中。我能够为我的片段注入 ViewModels(AndroidX 架构组件)。

我有一个 ViewPager,它有两个相同片段的实例(每个选项卡只有一个小的变化)并且在每个选项卡中,我观察到一个 LiveData获取数据变化的更新(来自 API)。

问题是,当 api 响应到来并更新 LiveData 时,当前可见片段中的相同数据将发送给所有选项卡中的观察者。 (我想这可能是因为ViewModel的范围)。

这是我观察数据的方式:

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

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

我正在使用这个 class 来提供 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"
    }
}

我这样绑定我的 ViewModel

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

我正在使用 @ContributesAndroidInjector 作为我的片段:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

我正在将这些模块添加到我的 MainActivity 子组件中,如下所示:

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

这是我的 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
    }
}

我正在扩展 DaggerFragment 并像这样注入 ViewModelProviderFactory

@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)
}

两个片段的 key 会有所不同。

如何确保只有当前片段的观察者得到更新。

编辑 1 ->

我添加了一个带有错误的示例项目。似乎只有在添加自定义范围时才会出现此问题。请在此处查看示例项目:Github link

master 分支有有问题的应用程序。如果您刷新任何选项卡(滑动刷新),更新后的值将反映在两个选项卡中。只有当我向它添加自定义范围时才会发生这种情况 (@MainScope)。

working_fine 分支有相同的应用程序,没有自定义范围并且工作正常。

如果问题不清楚,请告诉我。

两个片段都从 livedata 接收更新,因为 viewpager 将两个片段都保持在恢复状态。 由于您只需要对 viewpager 中可见的当前片段进行更新,因此 current 片段的上下文由主机 activity 定义,activity 应该明确直接更新到所需的片段。

您需要维护一个 Fragment 到 LiveData 的映射,其中包含添加到 viewpager 的所有片段的条目(确保具有可以区分同一片段的两个片段实例的标识符)。

现在 activity 将有一个 MediatorLiveData 直接观察片段观察到的原始实时数据。每当原始 livedata post 更新时,它将传递给 mediatorLivedata 并且 turen 中的 mediatorlivedata 只会 post 当前所选片段的 livedata 的值。此实时数据将从上面的地图中检索。

代码实现看起来像 -

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 
})
    }
}

我想重述一下原来的问题,就在这里:

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

据我了解,您的印象是,仅仅因为您试图使用不同的密钥来获取 ViewModel 的实例,那么应该为您提供 ViewModel 的不同实例:

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

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

现实,有点不同。如果您在片段中输入以下日志,您会看到这两个片段使用的是 PagerItemViewModel:

的完全相同的实例
Log.i("vvv", "${if (oneOrTwo) "one:" else "two:"} viewModel hash is ${viewModel.hashCode()}")

让我们深入了解为什么会发生这种情况。

在内部 ViewModelProvider#get() 将尝试从 ViewModelStore 获取 PagerItemViewModel 的实例,它基本上是 StringViewModel 的映射。

FirstFragment 请求 PagerItemViewModel 的实例时,map 为空,因此执行 mFactory.create(modelClass),最终在 ViewModelProviderFactory 中结束。 creator.get() 最终使用以下代码调用 DoubleCheck

  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;
  }

instance 现在是 null,因此 PagerItemViewModel 的新实例被创建并保存在 instance 中(参见 // 2)。

现在 SecondFragment 发生完全相同的过程:

  • 片段请求 PagerItemViewModel
  • 的实例
  • map 现在 not 是空的,但是 not 是否包含 PagerItemViewModel 的一个实例,键为 false
  • PagerItemViewModel 的一个新实例已启动,将通过 mFactory.create(modelClass)
  • 创建
  • 内部 ViewModelProviderFactory 执行到达 creator.get() 其实现是 DoubleCheck

现在,关键时刻。此 DoubleCheck DoubleCheck 的同一个实例 ,当 FirstFragment 要求时,它用于创建 ViewModel 实例。为什么是同一个实例?因为您已将范围应用于提供程序方法。

if (result == UNINITIALIZED) (// 1) 评估为 false 并且 ViewModel 的完全相同的实例被返回给调用者 - SecondFragment.

现在,两个片段都使用相同的 ViewModel 实例,因此它们显示相同的数据完全没问题。