Verwendung von Dolch 2 zum Injizieren von ViewModel derselben Fragmente in ViewPager

10

Ich versuche, Dagger 2 zu meinem Projekt hinzuzufügen. Ich konnte ViewModels (AndroidX Architecture-Komponente) für meine Fragmente einfügen.

Ich habe einen ViewPager mit 2 Instanzen desselben Fragments (nur eine geringfügige Änderung für jede Registerkarte) und in jeder Registerkarte beobachte ich eine LiveData, um über Datenänderungen (von der API) aktualisiert zu werden.

Das Problem ist, dass, wenn die API-Antwort kommt und die aktualisiert LiveData, dieselben Daten im aktuell sichtbaren Fragment an Beobachter auf allen Registerkarten gesendet werden. (Ich denke, das liegt wahrscheinlich am Umfang der ViewModel).

So beobachte ich meine Daten:

override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        activityViewModel.expenseList.observe(this, Observer {
            swipeToRefreshLayout.isRefreshing = false
            viewAdapter.setData(it)
        })
    ....
}

Ich benutze diese Klasse, um ViewModels bereitzustellen :

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

Ich binde meine ViewModelso:

@Module
abstract class ActivityViewModelModule {
    @MainScope
    @Binds
    @IntoMap
    @ViewModelKey(ActivityViewModel::class)
    abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}

Ich benutze @ContributesAndroidInjectorfür mein Fragment Folgendes:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

Und ich füge diese Module meiner MainActivityUnterkomponente folgendermaßen hinzu:

@Module
abstract class ActivityBuilderModule {
...
    @ContributesAndroidInjector(
        modules = [MainViewModelModule::class, ActivityViewModelModule::class,
            AuthModule::class, MainFragmentBuildersModule::class]
    )
    abstract fun contributeMainActivity(): MainActivity
}

Hier ist mein 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
    }
}

Ich dehne DaggerFragmentund spritze ViewModelProviderFactoryso:

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

Das keywird für beide Fragmente unterschiedlich sein.

Wie kann ich sicherstellen, dass nur der Beobachter des aktuellen Fragments aktualisiert wird?

BEARBEITEN 1 ->

Ich habe ein Beispielprojekt mit dem Fehler hinzugefügt. Das Problem tritt anscheinend nur auf, wenn ein benutzerdefinierter Bereich hinzugefügt wird. Bitte sehen Sie sich das Beispielprojekt hier an: Github-Link

masterZweig hat die App mit dem Problem. Wenn Sie eine Registerkarte aktualisieren (zum Aktualisieren wischen), wird der aktualisierte Wert in beiden Registerkarten angezeigt. Dies geschieht nur, wenn ich einen benutzerdefinierten Bereich hinzufüge ( @MainScope).

working_fine Branch hat die gleiche App ohne benutzerdefinierten Bereich und funktioniert einwandfrei.

Bitte lassen Sie mich wissen, wenn die Frage nicht klar ist.

hushed_voice
quelle
Ich verstehe nicht, warum Sie den Ansatz von working_fineBranch nicht verwenden werden. Warum brauchen Sie den Umfang?
Azizbekian
@azizbekian Ich verwende derzeit den Zweig "Working Fine". Aber ich möchte wissen, warum die Verwendung von Scope dies unterbrechen würde.
hushed_voice

Antworten:

1

Ich möchte die ursprüngliche Frage noch einmal zusammenfassen:

Ich verwende derzeit die Arbeit fine_branch, aber ich möchte wissen, warum die Verwendung von Scope dies unterbrechen würde.

Nach meinem Verständnis haben Sie den Eindruck, dass Sie, nur weil Sie versuchen, eine Instanz der ViewModelVerwendung verschiedener Schlüssel zu erhalten, verschiedene Instanzen von ViewModel:

// in first fragment
ViewModelProvider(...).get("true", PagerItemViewModel::class.java)

// in second fragment
ViewModelProvider(...).get("false", PagerItemViewModel::class.java)

Die Realität sieht etwas anders aus. Wenn Sie das folgende Anmeldefragment einfügen, werden Sie feststellen, dass diese beiden Fragmente genau dieselbe Instanz von verwenden PagerItemViewModel:

Log.i("vvv", "${if (oneOrTwo) "one:" else "two:"} viewModel hash is ${viewModel.hashCode()}")

Lassen Sie uns eintauchen und verstehen, warum dies passiert.

Intern ViewModelProvider#get()wird versucht, eine Instanz PagerItemViewModelvon a zu erhalten, ViewModelStoredie im Grunde eine Karte von Stringto ist ViewModel.

Wenn FirstFragmentfür eine Instanz von fragt PagerItemViewModeldie mapleer ist , damit mFactory.create(modelClass)ausgeführt wird, welches in endet ViewModelProviderFactory. creator.get()endet DoubleCheckmit folgendem Code:

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

Das instanceist jetzt null, daher wird eine neue Instanz von PagerItemViewModelerstellt und in gespeichert instance(siehe // 2).

Nun geschieht genau das gleiche Verfahren für SecondFragment:

  • Fragment fragt nach einer Instanz von PagerItemViewModel
  • mapjetzt ist nicht leer, aber nicht nicht eine Instanz enthält PagerItemViewModelmit Schlüsselnfalse
  • Eine neue Instanz von PagerItemViewModelwird initiiert, um über erstellt zu werdenmFactory.create(modelClass)
  • Innerhalb der ViewModelProviderFactoryAusführung erreicht, creator.get()wessen Implementierung istDoubleCheck

Nun, der Schlüsselmoment. Dies DoubleCheckist die gleiche Instanz von , DoubleCheckdass für die Erstellung verwendet wurde , ViewModelbeispielsweise , wenn FirstFragmentdanach gefragt. Warum ist es die gleiche Instanz? Weil Sie einen Bereich auf die Anbietermethode angewendet haben.

Das if (result == UNINITIALIZED)(// 1) wird als falsch ausgewertet und genau dieselbe Instanz von ViewModelwird an den Aufrufer zurückgegeben - SecondFragment.

Jetzt verwenden beide Fragmente dieselbe Instanz von, ViewModeldaher ist es vollkommen in Ordnung, dass sie dieselben Daten anzeigen.

Azizbekian
quelle
Danke für die Antwort. Das macht Sinn. Aber gibt es keine Möglichkeit, dies zu beheben, während Sie den Bereich verwenden?
hushed_voice
Das war vorher meine Frage: Warum müssen Sie Scope verwenden? Es ist, als ob Sie beim Bergsteigen ein Auto benutzen möchten und jetzt sagen Sie: "Okay, ich verstehe, warum ich kein Auto benutzen kann, aber wie kann ich ein Auto benutzen, um einen Berg zu besteigen?" Ihre Absichten sind nicht offensichtlich, bitte klären Sie.
Azizbekian
Vielleicht irre ich mich. Meine Erwartung war, dass die Verwendung von Scope ein besserer Ansatz ist. Zum Beispiel. Wenn meine Anwendung zwei Aktivitäten enthält (Login und Main), bei denen 1 benutzerdefinierter Bereich für die Anmeldung und 1 benutzerdefinierter Bereich für main verwendet werden, werden die unnötigen Instanzen entfernt, während eine Aktivität aktiv ist
hushed_voice
> Ich hatte erwartet, dass die Verwendung von Scope ein besserer Ansatz ist. Es ist nicht so, dass einer besser ist als der andere. Sie lösen unterschiedliche Probleme, jeder hat seinen Anwendungsfall.
Azizbekian
> entfernt die unnötigen Instanzen, während eine Aktivität aktiv ist. Kann nicht sehen, woher diese "unnötigen Instanzen" erstellt werden sollen. ViewModelwird mit dem Lebenszyklus der Aktivität / des Fragments erstellt und zerstört, sobald der Hosting-Lebenszyklus zerstört ist. Sie sollten den Lebenszyklus / die Zerstörung von ViewModel nicht selbst verwalten. Dies tun Architekturkomponenten für Sie als Client dieser API.
Azizbekian
0

Beide Fragmente erhalten das Update von Livedata, da Viewpager beide Fragmente im wiederaufgenommenen Zustand hält. Da Sie die Aktualisierung nur für das aktuelle Fragment benötigen, das im Viewpager angezeigt wird, wird der Kontext des aktuellen Fragments durch die Hostaktivität definiert. Die Aktivität sollte Aktualisierungen explizit auf das gewünschte Fragment richten.

Sie müssen eine Zuordnung von Fragment zu LiveData verwalten, die Einträge für alle Fragmente enthält (stellen Sie sicher, dass eine Kennung vorhanden ist, mit der zwei Fragmentinstanzen desselben Fragments unterschieden werden können), die dem Viewpager hinzugefügt wurde.

Jetzt hat die Aktivität eine MediatorLiveData, die die ursprünglichen Livedata beobachtet, die von den Fragmenten direkt beobachtet werden. Immer wenn die ursprünglichen Livedata ein Update veröffentlichen, wird es an mediatorLivedata gesendet, und die mediatorlivedata in Turen senden nur den Wert an Livedata des aktuell ausgewählten Fragments. Diese Livedata werden von der obigen Karte abgerufen.

Code impl würde aussehen wie -

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 
})
    }
}
Vishal Arora
quelle
Danke für die Antwort. Ich verwende FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)also, wie hält der Viewpager beide Fragmente im wieder aufgenommenen Zustand? Dies geschah nicht, bevor ich dem Projekt Dolch 2 hinzufügte.
hushed_voice
Ich werde versuchen, ein Beispielprojekt mit dem genannten Verhalten
hinzuzufügen
Hey, ich habe ein Beispielprojekt hinzugefügt. Kannst du es bitte überprüfen? Ich werde auch ein Kopfgeld dafür hinzufügen. (Entschuldigung für die Verzögerung)
hushed_voice
@hushed_voice Sicher wird sich bei Ihnen melden.
Vishal Arora