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


10

プロジェクトに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リンク

masterブランチには問題のあるアプリがあります。タブを更新すると(スワイプして更新)、更新された値が両方のタブに反映されます。これは、カスタムスコープを追加したときにのみ発生します(@MainScope)。

working_fine ブランチには同じアプリがあり、カスタムスコープはなく、正常に動作しています。

不明な点がある場合はお知らせください。


なぜworking_fineブランチからのアプローチを使用しないのですか?なぜスコープが必要なのですか?
アジズベキアン

@azizbekian私は現在、正常に機能しているブランチを使用しています。
hushed_voice

回答:


1

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

私は現在ワーキングを使用しています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()}")

これに飛び込んで、なぜこれが起こるの理解しましょう。

内部的には、基本的にtoのマップであるaのViewModelProvider#get()インスタンスを取得しようとします。PagerItemViewModelViewModelStoreStringViewModel

ときFirstFragmentのインスタンスを要求し、したがって、空であるに終わるれ、実行されます。最終的に次のコードで呼び出します:PagerItemViewModelmapmFactory.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現在は空ではありませが、PagerItemViewModelwithキーのインスタンスは含まれていませfalse
  • の新しいインスタンスPagerItemViewModelが作成され、mFactory.create(modelClass)
  • 内部ViewModelProviderFactory実行はcreator.get()、その実装がDoubleCheck

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

if (result == UNINITIALIZED)(// 1)偽とのまったく同じインスタンスに評価されているViewModel呼び出し元に戻されています- SecondFragment

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


答えてくれてありがとう。意味あり。しかし、スコープを使用しているときにこれを修正する方法はありませんか?
hushed_voice

それは以前私の質問でした:なぜスコープを使用する必要があるのですか?山を登るときに車を使いたくて「車が使えない理由はわかりますが、車を使って山に登るにはどうすればよいですか」と言われます。あなたの意図は明白ではありません、明確にしてください。
アジズベキアン

多分私は間違っています。私の期待は、スコープを使用する方が良いアプローチであるということでした。たとえば アプリケーションに2つのアクティビティ(ログインとメイン)がある場合、ログインに1つのカスタムスコープとメインに1つのカスタムスコープを使用すると、1つのアクティビティがアクティブなときに不要なインスタンスが削除されます
hushed_voice

>私の期待は、スコープを使用することの方が優れていることです。一方が他方よりも優れているわけではありません。彼らはさまざまな問題を解決しており、それぞれにユースケースがあります。
アジズベキアン

>は、1つのアクティビティがアクティブな間に不要なインスタンスを削除します。これらの「不要なインスタンス」をどこから作成する必要があるかがわかりません。ViewModelアクティビティ/フラグメントのライフサイクルで作成され、ホスティングライフサイクルが破棄されるとすぐに破棄されます。ViewModelのライフサイクル/作成破棄は自分で管理するべきではありません。それが、そのAPIのクライアントとしてアーキテクチャコンポーネントが実行していることです。
アジズベキアン

0

viewpagerは両方のフラグメントを再開状態に保つため、両方のフラグメントがlivedataから更新を受け取ります。だけviewpagerに表示現行断片に更新を必要とするので、のコンテキスト現在のフラグメントがホスト活性によって定義され、活性が所望のフラグメントへの明示的直接更新すべきです。

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

答えてくれてありがとう。私は使用しているFragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)ので、viewpagerは両方のフラグメントを再開状態にどのように維持していますか?これは、dagger 2をプロジェクトに追加する前には起こりませんでした。
hushed_voice

上記の動作のサンプルプロジェクトを追加してみます
hushed_voice

サンプルプロジェクトを追加しました。確認して頂けますか?私はこれに対する報奨金も追加します。(遅延して申し訳ありません)
hushed_voice

@hushed_voice必ず返信します。
Vishal Arora
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.