【问题标题】:How to use Dagger 2 to Inject ViewModel of same Fragments inside ViewPager如何使用 Dagger 2 在 ViewPager 中注入相同片段的 ViewModel
【发布时间】:2020-01-30 12:36:14
【问题描述】:

我正在尝试将 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)
        })
    ....
}

我正在使用这个类来提供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 分支具有相同的应用程序,没有自定义范围并且工作正常。

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

【问题讨论】:

  • 我不明白你为什么不使用working_fine 分支的方法?为什么需要范围?
  • @azizbekian 我目前正在使用工作正常的分支。但我想知道,为什么使用范围会破坏这个。

标签: android android-viewpager dagger-2 androidx android-mvvm


【解决方案1】:

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

我目前正在使用有效的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() 将尝试从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 是否包含带有键 falsePagerItemViewModel 的实例
  • PagerItemViewModel 的新实例已启动以通过mFactory.create(modelClass) 创建
  • ViewModelProviderFactory内部执行到达creator.get(),其实现为DoubleCheck

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

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

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

【讨论】:

  • 感谢您的回答。这是有道理的。但是在使用范围时没有办法解决这个问题吗?
  • 这是我之前的问题:为什么需要使用作用域?这就像你想在爬山时使用汽车,现在你说“好吧,我明白为什么我不能使用汽车,但是我怎么能用汽车爬山呢?” 你的意图并不明显,请澄清。
  • 也许我错了。我的期望是使用范围是一种更好的方法。例如。如果我的应用程序中有 2 个活动(登录和主),使用 1 个自定义范围用于登录,1 个自定义范围用于主,将在一个活动处于活动状态时删除不必要的实例
  • > 我的期望是使用范围是一种更好的方法。并不是说一个比另一个更好。他们正在解决不同的问题,每个都有自己的用例。
  • > 将在一项活动处于活动状态时删除不必要的实例 看不到应该从哪里创建这些“不必要的实例”。 ViewModel 是使用活动/片段的生命周期创建的,并且在其托管生命周期被销毁时立即被销毁。您不应该自己管理 ViewModel 的生命周期/创建销毁,这是架构组件作为该 API 的客户端为您做的事情。
【解决方案2】:

两个片段都从 livedata 接收更新,因为 viewpager 使两个片段都处于恢复状态。 由于您只需要在 viewpager 中可见的当前片段上进行更新,因此 当前 片段的上下文由宿主 Activity 定义,该 Activity 应明确将更新指向所需的片段。

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

现在活动将有一个 MediatorLiveData 观察片段直接观察到的原始实时数据。每当原始 livedata 发布更新时,它都会被传递到 mediatorLivedata,然后 mediatorlivedata 只会将值发布到当前所选片段的 livedata。将从上面的地图中检索此实时数据。

代码 impl 看起来像 -

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

【讨论】:

  • 感谢您的回答。我正在使用 FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) 那么 viewpager 如何将两个片段保持在恢复状态?在我将 dagger 2 添加到项目中之前,这并没有发生。
  • 我会尝试添加一个具有上述行为的示例项目
  • 嘿,我添加了一个示例项目。你能检查一下吗。我也会为此添加赏金。(抱歉耽搁了)
  • @hushed_voice 当然会回复你。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-08
  • 1970-01-01
  • 1970-01-01
  • 2018-03-08
  • 1970-01-01
相关资源
最近更新 更多