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


10

我正在尝试将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 分支具有相同的应用程序,没有自定义范围,并且可以正常工作。

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


我不明白为什么你不使用working_fine分支机构的方法?为什么需要示波器?
阿齐兹别克人

@azizbekian我当前正在使用精细分支。但是我想知道,为什么使用范围会破坏此分支。
hushed_voice

Answers:


1

我想回顾一下原来的问题,就是这样:

我目前正在使用work 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()将尝试获得的实例PagerItemViewModel从一个ViewModelStore基本上是地图StringViewModel

FirstFragment用于实例要求PagerItemViewModelmap是空的,因此mFactory.create(modelClass)被执行,其中在结束了ViewModelProviderFactorycreator.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;
  }

现在instancenull,因此将PagerItemViewModel创建的新实例并将其保存在中instance(请参见// 2)。

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

  • 片段要求一个实例 PagerItemViewModel
  • map现在是不是空的,但并没有包含的实例PagerItemViewModel与关键false
  • PagerItemViewModel发起一个新实例,以通过创建mFactory.create(modelClass)
  • 内部ViewModelProviderFactory执行达到creator.get()其实现是DoubleCheck

现在,关键时刻。这DoubleCheck同一个实例DoubleCheck用于创建ViewModel当实例FirstFragment自找的。为什么是同一实例?因为您已将范围应用到provider方法。

if (result == UNINITIALIZED)(// 1)评估为false和完全相同的情况下ViewModel被返回给调用者- SecondFragment

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


感谢您的回答。这很有道理。但是在使用范围时是否没有办法解决此问题?
hushed_voice

这以前是我的问题:为什么需要使用范围?就像您要在爬山时使用汽车,现在您说的是:“好,我明白为什么我不能使用汽车,但是我怎么能用汽车爬山呢?” 您的意图不明显,请澄清。
阿齐兹别克人

也许我错了。我的期望是使用范围是一种更好的方法。例如。如果我的应用程序中有2个活动(“登录”和“主”),则使用1个自定义范围进行登录,并使用1个自定义范围作为主活动,将在一个活动处于活动状态时删除不必要的实例
hushed_voice

>我的期望是使用范围是一种更好的方法,并不是说一个范围比另一个更好。他们正在解决不同的问题,每个问题都有其用例。
阿齐兹别克人

>将在一个活动处于活动状态时删除不必要的实例。看不到应该从那些“不必要的实例”中创建位置。ViewModel使用活动/片段的生命周期创建,并在其宿主生命周期被破坏时被破坏。您不应该自己管理ViewModel的生命周期/创建销毁过程,这就是架构组件作为该API客户端为您所做的工作。
阿齐兹别克人

0

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

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

现在,该活动将具有一个MediatorLiveData,它可以直接观察片段观察到的原始实时数据。每当原始实时数据发布更新时,它将被发送到mediatorLivedata,而turen中的mediatorlivedata仅将值发布到当前所选片段的实时数据中。该实时数据将从上面的地图中检索。

代码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如何将两个片段都保持在恢复状态?在我向项目添加匕首2之前,这没有发生。
hushed_voice

我将尝试添加一个具有上述行为的示例项目
hushed_voice

嘿,我添加了一个示例项目。你能检查一下吗?我还将为此提供赏金。(抱歉,延迟)
hushed_voice

@hushed_voice当然可以给您回复。
维沙尔·阿罗拉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.