首页 > 解决方案 > 如何使用 Dagger 2 在 ViewPager 中注入相同片段的 ViewModel

问题描述

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

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

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

这就是我观察数据的方式:

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

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

我正在使用这个类来提供ViewModel

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 链接

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

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

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

标签: androidandroid-viewpagerdagger-2androidxandroid-mvvm

解决方案


我想回顾一下最初的问题,这里是:

我目前正在使用 working fine_branch,但我想知道,为什么使用范围会破坏这一点。

根据我的理解,您的印象是,仅仅因为您试图获取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()将尝试获取PagerItemViewModelfrom a的实例,该实例ViewModelStore基本上是Stringto的映射ViewModel

FirstFragment请求的实例PagerItemViewModelmap空时,因此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是 now ,因此会创建null一个新的实例PagerItemViewModel并保存在其中instance(参见 // 2)。

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

  • 片段要求一个实例PagerItemViewModel
  • mapnow为空,但不包含with PagerItemViewModelkey的实例false
  • 一个新的实例PagerItemViewModel被发起创建通过mFactory.create(modelClass)
  • 内部ViewModelProviderFactory执行达到creator.get()谁的实现是DoubleCheck

现在,关键时刻。这与请求DoubleCheck时用于创建实例的实例相同。为什么是同一个实例?因为您已将范围应用于提供程序方法。DoubleCheckViewModelFirstFragment

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

现在,两个片段都使用相同的实例,ViewModel因此它们显示相同的数据非常好。


推荐阅读