プロジェクトにDagger 2を追加しようとしています。フラグメントにViewModels(AndroidXアーキテクチャコンポーネント)を注入することができました。
同じフラグメントの2つのインスタンスを持つ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)
})
....
}
私は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
ブランチには、カスタムスコープのない同じアプリがあり、正常に動作しています。
不明な点がありましたらお知らせください。
私は元の質問を要約したいと思います、これはそれです:
私は現在作業中の_
fine_branch
_を使用していますが、なぜスコープを使用するとこれが壊れるのか知りたいです。
私の理解によると、あなたは印象を持っています。それは、異なるキーを使用してViewModel
のインスタンスを取得しようとしているからといって、ViewModel
の異なるインスタンスを提供する必要があります。
_// in first fragment
ViewModelProvider(...).get("true", PagerItemViewModel::class.Java)
// in second fragment
ViewModelProvider(...).get("false", PagerItemViewModel::class.Java)
_
現実は少し異なります。次のログインフラグメントを置くと、これら2つのフラグメントがPagerItemViewModel
のまったく同じインスタンスを使用していることがわかります。
_Log.i("vvv", "${if (oneOrTwo) "one:" else "two:"} viewModel hash is ${viewModel.hashCode()}")
_
これに飛び込んで、なぜなのかを理解しましょう。
内部的にViewModelProvider#get()
は、基本的にPagerItemViewModel
からViewModelStore
へのマップである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
の作成時に使用されたViewModel
のFirstFragment
が要求したときに使用されました。なぜ同じインスタンスなのですか?プロバイダーメソッドにスコープを適用したからです。
if (result == UNINITIALIZED)
(// 1)はfalseと評価され、ViewModel
のまったく同じインスタンスが呼び出し元に返されます-SecondFragment
。
これで、両方のフラグメントがViewModel
の同じインスタンスを使用しているため、同じデータを表示していることはまったく問題ありません。
Viewpagerは両方のフラグメントを再開状態に保つため、両方のフラグメントがlivedataから更新を受け取ります。ビューページャーに表示されている現在のフラグメントのみを更新する必要があるため、currentフラグメントのコンテキストはホストアクティビティによって定義され、アクティビティは更新を目的のフラグメントに明示的に送信する必要があります。
Viewpagerに追加されたすべてのフラグメントのエントリを含むLiveDataへのフラグメントのマップを維持する必要があります(同じフラグメントの2つのフラグメントインスタンスを区別できる識別子があることを確認してください)。
これで、アクティビティはMediatorLiveDataを持ち、フラグメントによって直接観察される元のライブデータを観察します。元のlivedataが更新を送信するときはいつでも、それはmediatorLivedataに配信され、turenのmediatorlivedataは現在選択されているフラグメントの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
})
}
}