J'essaye d'ajouter Dagger 2 à mon projet. J'ai pu injecter des 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 les changements de données (à partir de l'API).
Le problème est que lorsque la réponse de l'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 du 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 sous-composant MainActivity
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'étend 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.
MODIFIER 1 ->
J'ai ajouté un exemple de projet avec l'erreur. On dirait que le problème ne se produit que lorsqu'une étendue personnalisée est ajoutée. Veuillez consulter l'exemple de projet ici: lien Github
master
branch a l'application avec le problème. Si vous actualisez un onglet (faites glisser pour actualiser), la valeur mise à jour est reflétée dans les deux onglets. Cela ne se produit que lorsque j'y ajoute une portée personnalisée (@MainScope
).
working_fine
branch a la même application sans portée personnalisée et fonctionne correctement.
Veuillez me faire savoir si la question n'est pas claire.
Je veux récapituler la question initiale, la voici:
J'utilise actuellement le
fine_branch
De travail, mais je veux savoir pourquoi l'utilisation de la portée briserait cela.
D'après ce que j'ai compris, vous avez l'impression que, simplement parce que vous essayez d'obtenir une instance de ViewModel
en utilisant différentes clés, vous devriez alors vous fournir 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 journal suivant dans le fragment, 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 PagerItemViewModel
à partir d'un ViewModelStore
qui est essentiellement une carte de String
vers ViewModel
.
Lorsque FirstFragment
demande une instance de PagerItemViewModel
, le map
est vide, donc mFactory.create(modelClass)
est exécuté, ce qui se termine par ViewModelProviderFactory
. creator.get()
finit par appeler DoubleCheck
avec 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 instance
est maintenant null
, donc une nouvelle instance de PagerItemViewModel
est créée et est enregistrée dans instance
(voir // 2).
Maintenant, la même procédure se produit pour SecondFragment
:
PagerItemViewModel
map
maintenant non vide, mais pas contient une instance de PagerItemViewModel
avec la clé false
PagerItemViewModel
est lancée pour être créée via mFactory.create(modelClass)
ViewModelProviderFactory
l'exécution atteint creator.get()
dont l'implémentation est DoubleCheck
Maintenant, le moment clé. Cette DoubleCheck
est la même instance de DoubleCheck
qui a été utilisée pour créer l'instance ViewModel
lorsque FirstFragment
l'a demandé. Pourquoi est-ce le même cas? Parce que vous avez appliqué une étendue à la méthode du fournisseur.
La if (result == UNINITIALIZED)
(// 1) est évaluée à false et la même instance exacte de ViewModel
est renvoyée à l'appelant - SecondFragment
.
Maintenant, les deux fragments utilisent la même instance de ViewModel
donc il est parfaitement normal qu'ils affichent les mêmes données.
Les deux fragments reçoivent la mise à jour de livingata car le viewpager maintient les deux fragments dans l'état de reprise. Puisque vous avez besoin de la mise à jour uniquement sur le fragment actuel visible dans le visualiseur, le contexte du fragment actuel est défini par l'activité Hôte, l'activité doit explicitement diriger les mises à jour vers le fragment souhaité.
Vous devez maintenir une carte de Fragment vers LiveData contenant les 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.
Désormais, l'activité aura un MediatorLiveData observant directement le vécu original observé par les fragments. Chaque fois que le liveata original publie une mise à jour, il sera livré à mediatorLivedata et le mediatorlivedata in turen ne publiera que la valeur à vivreata du fragment actuellement sélectionné. Ces données 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
})
}
}