如何使用 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 来提供 ViewModel
s:
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
的实例,它基本上是 String
到 ViewModel
的映射。
当 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
实例,因此它们显示相同的数据完全没问题。
我正在尝试将 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 来提供 ViewModel
s:
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
的实例,它基本上是 String
到 ViewModel
的映射。
当 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
实例,因此它们显示相同的数据完全没问题。