Come usare Dagger 2 per iniettare ViewModel degli stessi frammenti all'interno di ViewPager


10

Sto cercando di aggiungere Dagger 2 al mio progetto. Sono stato in grado di iniettare ViewModels (componente AndroidX Architecture) per i miei frammenti.

Ho un ViewPager che ha 2 istanze dello stesso frammento (solo una piccola modifica per ciascuna scheda) e in ciascuna scheda, sto osservando un LiveDataaggiornamento per la modifica dei dati (dall'API).

Il problema è che quando arriva la risposta api e la aggiorna LiveData, gli stessi dati nel frammento attualmente visibile vengono inviati agli osservatori in tutte le schede. (Penso che questo sia probabilmente a causa della portata del ViewModel).

Ecco come sto osservando i miei dati:

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

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

Sto usando questa classe per fornire ViewModel:

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

Sto vincolando il mio in ViewModelquesto modo:

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

Sto usando @ContributesAndroidInjectorper il mio frammento in questo modo:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

E sto aggiungendo questi moduli al mio MainActivitysottocomponente in questo modo:

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

Ecco il mio 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
    }
}

Sto estendendo DaggerFragmente iniettando in ViewModelProviderFactoryquesto modo:

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

il keysarà diverso per entrambi i frammenti.

Come posso assicurarmi che solo l'osservatore del frammento attuale venga aggiornato.

MODIFICA 1 ->

Ho aggiunto un progetto di esempio con l'errore. Sembra che il problema si stia verificando solo quando viene aggiunto un ambito personalizzato. Controlla il progetto di esempio qui: link Github

masterbranch ha l'app con il problema. Se aggiorni una scheda (scorri per aggiornare), il valore aggiornato viene riflesso in entrambe le schede. Questo succede solo quando aggiungo un ambito personalizzato ( @MainScope).

working_fine branch ha la stessa app senza ambito personalizzato e funziona bene.

Per favore fatemi sapere se la domanda non è chiara.


Non capisco perché non usi l'approccio da working_finebranch? Perché hai bisogno dell'ambito?
Azizbekian

@azizbekian Attualmente sto usando il ramo funzionante bene .. ma voglio sapere, perché usare l'ambito avrebbe rotto questo.
hushed_voice,

Risposte:


1

Voglio ricapitolare la domanda originale, eccola qui:

Attualmente sto usando il lavoro fine_branch, ma voglio sapere, perché l'utilizzo dell'ambito romperebbe questo.

Secondo la mia comprensione, hai un'impressione, che solo perché stai cercando di ottenere un'istanza di ViewModelutilizzo di chiavi diverse, allora dovresti ricevere diverse istanze di ViewModel:

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

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

La realtà è un po 'diversa. Se inserisci il seguente frammento di log, vedrai che quei due frammenti stanno usando esattamente la stessa istanza di PagerItemViewModel:

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

Immergiamoci e capiamo perché questo accade.

Internamente ViewModelProvider#get()tenterà di ottenere un'istanza di PagerItemViewModelda a ViewModelStoreche è sostanzialmente una mappa di Stringa ViewModel.

Quando FirstFragmentchiede un'istanza PagerItemViewModeldel mapvuoto, quindi mFactory.create(modelClass)viene eseguito, che finisce in ViewModelProviderFactory. creator.get()finisce per chiamare DoubleCheckcon il seguente codice:

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

Il instanceè ora null, quindi una nuova istanza PagerItemViewModelviene creata e memorizzata nella instance(vedi // 2).

Ora accade la stessa identica procedura per SecondFragment:

  • il frammento richiede un'istanza di PagerItemViewModel
  • mapora non è vuoto, ma non contiene un'istanza di PagerItemViewModelcon chiavefalse
  • PagerItemViewModelviene avviata una nuova istanza di da creare tramitemFactory.create(modelClass)
  • L' ViewModelProviderFactoryesecuzione interna raggiunge la creator.get()cui implementazione èDoubleCheck

Ora, il momento chiave. Questa DoubleCheckè la stessa istanza di DoubleCheckche è stato utilizzato per la creazione ViewModelesempio quando FirstFragmentchiesto. Perché è la stessa istanza? Perché hai applicato un ambito al metodo del provider.

Il if (result == UNINITIALIZED)(// 1) sta valutando false e la stessa istanza ViewModelviene restituito al chiamante - SecondFragment.

Ora, entrambi i frammenti stanno usando la stessa istanza, ViewModelquindi è perfettamente corretto visualizzare gli stessi dati.


Grazie per la risposta. Questo ha senso. Ma non c'è un modo per risolvere questo problema durante l'utilizzo dell'ambito?
hushed_voice

Questa era la mia domanda in precedenza: perché è necessario utilizzare l'ambito? È come se volessi usare una macchina quando scalassi una montagna e ora stai dicendo "ok, capisco perché non posso usare una macchina, ma come posso usare una macchina per scalare una montagna?" Le tue intenzioni non sono ovvie, ti preghiamo di chiarire.
Azizbekian

Forse mi sbaglio. La mia aspettativa era che l'uso dell'ambito fosse un approccio migliore. Per es. Se ci sono 2 attività nella mia applicazione (Login e Main), utilizzando 1 ambito personalizzato per l'accesso e 1 ambito personalizzato per principale, rimuoveranno le istanze non necessarie mentre un'attività è attiva
hushed_voice

> La mia aspettativa era che l'uso dell'ambito fosse un approccio migliore Non è che uno sia migliore dell'altro. Stanno risolvendo problemi diversi, ognuno ha il proprio caso d'uso.
Azizbekian

> rimuoverà le istanze non necessarie mentre un'attività è attiva Impossibile vedere da dove dovrebbero essere create quelle "istanze non necessarie". ViewModelviene creato con il ciclo di vita dell'attività / frammento e viene distrutto non appena viene distrutto il ciclo di vita dell'host. Non dovresti gestire da solo il ciclo di vita / la distruzione della creazione di ViewModel, ecco cosa fanno i componenti dell'architettura come client di quell'API.
Azizbekian

0

Entrambi i frammenti ricevono l'aggiornamento daivedata perché viewpager mantiene entrambi i frammenti nello stato ripreso. Poiché è necessario l'aggiornamento solo sul frammento corrente visibile nel viewpager, il contesto del frammento corrente è definito dall'attività host, l'attività dovrebbe indirizzare esplicitamente gli aggiornamenti al frammento desiderato.

È necessario mantenere una mappa di frammento su LiveData contenente le voci per tutti i frammenti (assicurarsi di avere un identificatore in grado di differenziare due istanze di frammento dello stesso frammento) aggiunto a viewpager.

Ora l'attività avrà un MediatorLiveData che osserva direttamente i vissata originali osservati dai frammenti. Ogni volta che la vissata originale pubblica un aggiornamento, verrà consegnata a mediatorLivedata e la mediatorlivedata in turen pubblicherà solo il valore suivedata dell'attuale frammento selezionato. Questa vissuta verrà recuperata dalla mappa sopra.

Il codice impl sembrerebbe -

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

Grazie per la risposta. Sto usando FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)quindi come fa viewpager a mantenere entrambi i frammenti nello stato di ripresa? Questo non stava accadendo prima che aggiungessi il pugnale 2 al progetto.
hushed_voice

Proverò ad aggiungere un progetto di esempio con il suddetto comportamento
hushed_voice

Ehi, ho aggiunto un progetto di esempio. Puoi per favore controllarlo. Aggiungerò anche una taglia per questo. (
Ci

@hushed_voice Certo ti risponderà.
Vishal Arora,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.