Comment utiliser Dagger 2 pour injecter ViewModel des mêmes fragments dans ViewPager

10

J'essaie d'ajouter Dagger 2 à mon projet. J'ai pu injecter ViewModels (composant AndroidX Architecture) pour mes fragments.

J'ai un ViewPager qui a 2 instances du même fragment (seulement un changement mineur pour chaque onglet) et dans chaque onglet, j'observe un LiveDatapour être mis à jour sur le changement de données (à partir de l'API).

Le problème est que lorsque la réponse api arrive et met à jour le LiveData, les mêmes données dans le fragment actuellement visible sont envoyées aux observateurs dans tous les onglets. (Je pense que c'est probablement à cause de la portée de la ViewModel).

Voici comment j'observe mes données:

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

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

J'utilise cette classe pour fournir 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"
    }
}

Je lie mon ViewModelcomme ceci:

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

J'utilise @ContributesAndroidInjectorpour mon fragment comme ceci:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

Et j'ajoute ces modules à mon MainActivitysous - composant comme ceci:

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

Voici mon 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
    }
}

J'étends DaggerFragmentet j'injecte ViewModelProviderFactorycomme ceci:

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

le keysera différent pour les deux fragments.

Comment puis-je m'assurer que seul l'observateur du fragment actuel est mis à jour.

EDIT 1 ->

J'ai ajouté un exemple de projet avec l'erreur. Il semble que le problème se produit uniquement lorsqu'une étendue personnalisée est ajoutée. Veuillez consulter l'exemple de projet ici: Lien Github

masterbranche a l'application avec le problème. Si vous actualisez un onglet (faites glisser pour actualiser), la valeur mise à jour se reflète dans les deux onglets. Cela ne se produit que lorsque j'y ajoute une étendue personnalisée ( @MainScope).

working_fine branche a la même application sans portée personnalisée et fonctionne bien.

Veuillez me faire savoir si la question n'est pas claire.

hushed_voice
la source
Je ne comprends pas pourquoi n'utilisez-vous pas l'approche de la working_finebranche? Pourquoi avez-vous besoin de la portée?
Azizbekian
@azizbekian J'utilise actuellement la branche fine de travail .. mais je veux savoir pourquoi l'utilisation de la portée casserait cela.
hushed_voice

Réponses:

1

Je veux récapituler la question d'origine, la voici:

J'utilise actuellement le travail fine_branch, mais je veux savoir pourquoi l'utilisation de la portée casserait cela.

Selon ma compréhension, vous avez l'impression que, simplement parce que vous essayez d'obtenir une instance d' ViewModelutilisation de différentes clés, vous devez disposer de différentes instances de ViewModel:

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

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

La réalité est un peu différente. Si vous mettez le fragment de connexion suivant, vous verrez que ces deux fragments utilisent exactement la même instance de PagerItemViewModel:

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

Plongeons-nous et comprenons pourquoi cela se produit.

En interne ViewModelProvider#get(), essaiera d'obtenir une instance de à PagerItemViewModelpartir de ViewModelStorelaquelle est essentiellement une carte de Stringà ViewModel.

Quand FirstFragmentdemande une instance de PagerItemViewModell' mapest vide, d' où mFactory.create(modelClass)est exécuté, qui se termine en ViewModelProviderFactory. creator.get()finit par appeler DoubleCheckavec le code suivant:

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

Le instanceest maintenant null, donc une nouvelle instance de PagerItemViewModelest créée et enregistrée dans instance(voir // 2).

Maintenant, la même procédure exacte se produit pour SecondFragment:

  • fragment demande une instance de PagerItemViewModel
  • mapnow n'est pas vide, mais ne contient pas d'instance de PagerItemViewModelwith keyfalse
  • une nouvelle instance de PagerItemViewModelest lancée pour être créée viamFactory.create(modelClass)
  • L' ViewModelProviderFactoryexécution interne atteint creator.get()dont la mise en œuvre estDoubleCheck

Maintenant, le moment clé. DoubleCheckC'est la même instance de DoubleCheckqui a été utilisé pour créer par ViewModelexemple lorsque FirstFragmentdemandé. Pourquoi est-ce la même instance? Parce que vous avez appliqué une étendue à la méthode du fournisseur.

Le if (result == UNINITIALIZED)(// 1) évalue à faux et la même instance exacte de ViewModelest d' être renvoyé à l'appelant - SecondFragment.

Maintenant, les deux fragments utilisent la même instance, ViewModelil est donc parfaitement correct qu'ils affichent les mêmes données.

azizbekian
la source
Merci d'avoir répondu. C'est logique. Mais n'y a-t-il pas un moyen de résoudre ce problème lors de l'utilisation de la portée?
hushed_voice
C'était ma question précédemment: pourquoi avez-vous besoin d'utiliser la portée? C'est comme si vous vouliez utiliser une voiture pour gravir une montagne et maintenant vous dites "ok, je comprends pourquoi je ne peux pas utiliser une voiture, mais comment puis-je utiliser une voiture pour gravir une montagne?" Vos intentions ne sont pas évidentes, veuillez clarifier.
Azizbekian
Peut-être que je me trompe. Je m'attendais à ce que l'utilisation de la portée soit une meilleure approche. Par exemple. S'il y a 2 activités dans mon application (Login et Main), utiliser 1 portée personnalisée pour la connexion et 1 portée personnalisée pour principal, supprimera les instances inutiles pendant qu'une activité est active
hushed_voice
> Je m'attendais à ce que l'utilisation de la portée soit une meilleure approche Ce n'est pas que l'un est meilleur que l'autre. Ils résolvent différents problèmes, chacun a son cas d'utilisation.
Azizbekian
> supprimera les instances inutiles lorsqu'une activité est active Impossible de voir d'où ces "instances inutiles" doivent être créées. ViewModelest créé avec le cycle de vie de l'activité / du fragment et est détruit dès qu'il héberge le cycle de vie. Vous ne devez pas gérer vous-même le cycle de vie / la destruction de la création de ViewModel, c'est ce que les composants d'architecture font pour vous en tant que client de cette API.
Azizbekian
0

Les deux fragments reçoivent la mise à jour de liveata car le visualiseur conserve les deux fragments à l'état repris. Étant donné que vous avez besoin de la mise à jour uniquement sur le fragment actuel visible dans le viewpager, le contexte de la fragment actuel est défini par l'activité de l'hôte, l'activité doit diriger explicitement les mises à jour vers le fragment souhaité.

Vous devez conserver une mappe de Fragment à LiveData contenant des entrées pour tous les fragments (assurez-vous d'avoir un identifiant qui peut différencier deux instances de fragment du même fragment) ajouté à viewpager.

Maintenant, l'activité aura un MediatorLiveData observant directement les données de vie originales observées par les fragments. Chaque fois que les livesata originaux publient une mise à jour, ils seront livrés à mediatorLivedata et les mediatorlivedata in turen ne publieront que la valeur de livesata du fragment sélectionné. Ces données de vie seront récupérées sur la carte ci-dessus.

Le code impl ressemblerait à -

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
la source
Merci d'avoir répondu. J'utilise FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)alors comment le visualiseur garde-t-il les deux fragments à l'état repris? Cela ne se produisait pas avant d'ajouter la dague 2 au projet.
hushed_voice
Je vais essayer d'ajouter un exemple de projet avec le dit comportement
hushed_voice
Hé, j'ai ajouté un exemple de projet. Pouvez-vous s'il vous plaît le vérifier. J'ajouterai également une prime pour cela. (Désolé pour le retard)
hushed_voice
@hushed_voice Sure vous répondra.
Vishal Arora