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 LiveData
pour ê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 ViewModel
s:
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 ViewModel
comme ceci:
@Module
abstract class ActivityViewModelModule {
@MainScope
@Binds
@IntoMap
@ViewModelKey(ActivityViewModel::class)
abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}
J'utilise @ContributesAndroidInjector
pour mon fragment comme ceci:
@Module
abstract class MainFragmentBuildersModule {
@ContributesAndroidInjector
abstract fun contributeActivityFragment(): ActivityFragment
}
Et j'ajoute ces modules à mon MainActivity
sous - 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 DaggerFragment
et j'injecte ViewModelProviderFactory
comme 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 key
sera 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
master
branche 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.
la source
working_fine
branche? Pourquoi avez-vous besoin de la portée?Réponses:
Je veux récapituler la question d'origine, la voici:
Selon ma compréhension, vous avez l'impression que, simplement parce que vous essayez d'obtenir une instance d'
ViewModel
utilisation de différentes clés, vous devez disposer de différentes instances deViewModel
: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
:Plongeons-nous et comprenons pourquoi cela se produit.
En interne
ViewModelProvider#get()
, essaiera d'obtenir une instance de àPagerItemViewModel
partir deViewModelStore
laquelle est essentiellement une carte deString
àViewModel
.Quand
FirstFragment
demande une instance dePagerItemViewModel
l'map
est vide, d' oùmFactory.create(modelClass)
est exécuté, qui se termine enViewModelProviderFactory
.creator.get()
finit par appelerDoubleCheck
avec le code suivant:Le
instance
est maintenantnull
, donc une nouvelle instance dePagerItemViewModel
est créée et enregistrée dansinstance
(voir // 2).Maintenant, la même procédure exacte se produit pour
SecondFragment
:PagerItemViewModel
map
now n'est pas vide, mais ne contient pas d'instance dePagerItemViewModel
with keyfalse
PagerItemViewModel
est lancée pour être créée viamFactory.create(modelClass)
ViewModelProviderFactory
exécution interne atteintcreator.get()
dont la mise en œuvre estDoubleCheck
Maintenant, le moment clé.
DoubleCheck
C'est la même instance deDoubleCheck
qui a été utilisé pour créer parViewModel
exemple lorsqueFirstFragment
demandé. 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 deViewModel
est d' être renvoyé à l'appelant -SecondFragment
.Maintenant, les deux fragments utilisent la même instance,
ViewModel
il est donc parfaitement correct qu'ils affichent les mêmes données.la source
ViewModel
est 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.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 à -
la source
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.