web-dev-qa-db-ja.com

Dagger 2を使用してViewPager内に同じフラグメントのViewModelを挿入する方法

プロジェクトに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)
        })
    ....
}

私は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ブランチには、カスタムスコープのない同じアプリがあり、正常に動作しています。

不明な点がありましたらお知らせください。

10
hushed_voice

私は元の質問を要約したいと思います、これはそれです:

私は現在作業中の_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のインスタンスを取得しようとします。

FirstFragmentPagerItemViewModelのインスタンスを要求する場合、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;
  }
_

instancenullになりました。したがって、PagerItemViewModelの新しいインスタンスが作成され、instanceに保存されます(// 2を参照)。

まったく同じ手順がSecondFragmentでも発生します。

  • フラグメントはPagerItemViewModelのインスタンスを要求します
  • mapnot空ですが、notは空ですキーPagerItemViewModelを持つfalseのインスタンスを含む
  • PagerItemViewModelの新しいインスタンスがmFactory.create(modelClass)を介して作成されます
  • 内部でViewModelProviderFactoryの実行はcreator.get()に達し、その実装はDoubleCheckです

今、重要な瞬間。このDoubleCheck同じインスタンスDoubleCheckの作成時に使用されたViewModelFirstFragmentが要求したときに使用されました。なぜ同じインスタンスなのですか?プロバイダーメソッドにスコープを適用したからです。

if (result == UNINITIALIZED)(// 1)はfalseと評価され、ViewModelのまったく同じインスタンスが呼び出し元に返されます-SecondFragment

これで、両方のフラグメントがViewModelの同じインスタンスを使用しているため、同じデータを表示していることはまったく問題ありません。

2
azizbekian

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 
})
    }
}
0
Vishal Arora