Estoy tratando de agregar Dagger 2 a mi proyecto. Pude inyectar ViewModels (componente de Arquitectura AndroidX) para mis fragmentos.
Tengo un ViewPager que tiene 2 instancias del mismo fragmento (solo un cambio menor para cada pestaña) y en cada pestaña, estoy observando una LiveData
actualización sobre el cambio de datos (desde la API).
El problema es que cuando llega la respuesta de la API y la actualiza LiveData
, los mismos datos en el fragmento actualmente visible se envían a los observadores en todas las pestañas. (Creo que esto es probablemente debido al alcance de la ViewModel
).
Así es como estoy observando mis datos:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
activityViewModel.expenseList.observe(this, Observer {
swipeToRefreshLayout.isRefreshing = false
viewAdapter.setData(it)
})
....
}
Estoy usando esta clase para proporcionar 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"
}
}
Estoy vinculando mi ViewModel
así:
@Module
abstract class ActivityViewModelModule {
@MainScope
@Binds
@IntoMap
@ViewModelKey(ActivityViewModel::class)
abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
}
Estoy usando @ContributesAndroidInjector
para mi fragmento así:
@Module
abstract class MainFragmentBuildersModule {
@ContributesAndroidInjector
abstract fun contributeActivityFragment(): ActivityFragment
}
Y estoy agregando estos módulos a mi MainActivity
subcomponente así:
@Module
abstract class ActivityBuilderModule {
...
@ContributesAndroidInjector(
modules = [MainViewModelModule::class, ActivityViewModelModule::class,
AuthModule::class, MainFragmentBuildersModule::class]
)
abstract fun contributeMainActivity(): MainActivity
}
Aquí está mi 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
}
}
Estoy extendiendo DaggerFragment
e inyectando ViewModelProviderFactory
así:
@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)
}
El key
será diferente para ambos fragmentos.
¿Cómo puedo asegurarme de que solo el observador del fragmento actual se esté actualizando?
EDITAR 1 ->
He agregado un proyecto de muestra con el error. Parece que el problema ocurre solo cuando se agrega un ámbito personalizado. Consulte el proyecto de muestra aquí: enlace de Github
master
Branch tiene la aplicación con el problema. Si actualiza cualquier pestaña (deslice para actualizar), el valor actualizado se refleja en ambas pestañas. Esto solo sucede cuando le agrego un alcance personalizado ( @MainScope
).
working_fine
Branch tiene la misma aplicación sin alcance personalizado y funciona bien.
Avíseme si la pregunta no está clara.
fuente
working_fine
rama? ¿Por qué necesitas el alcance?Respuestas:
Quiero recapitular la pregunta original, aquí está:
Según tengo entendido, usted tiene la impresión de que solo porque está tratando de obtener una instancia de
ViewModel
uso de diferentes claves, entonces debería recibir diferentes instancias deViewModel
:La realidad es un poco diferente. Si coloca el siguiente fragmento de inicio de sesión, verá que esos dos fragmentos están utilizando exactamente la misma instancia de
PagerItemViewModel
:Vamos a sumergirnos y entender por qué sucede esto.
ViewModelProvider#get()
Intentará obtener internamente una instancia dePagerItemViewModel
aViewModelStore
que básicamente es un mapa deString
aViewModel
.Cuando
FirstFragment
solicita una instancia dePagerItemViewModel
lamap
está vacía, pormFactory.create(modelClass)
lo tanto, se ejecuta, lo que termina enViewModelProviderFactory
.creator.get()
termina llamandoDoubleCheck
con el siguiente código:El
instance
es ahoranull
, por lo tanto,PagerItemViewModel
se crea una nueva instancia de y se guarda eninstance
(ver // 2).Ahora ocurre exactamente el mismo procedimiento para
SecondFragment
:PagerItemViewModel
map
ahora no está vacío, pero no contiene una instancia dePagerItemViewModel
con clavefalse
PagerItemViewModel
se inicia una nueva instancia de a través demFactory.create(modelClass)
ViewModelProviderFactory
ejecución interna alcanzacreator.get()
cuya implementación esDoubleCheck
Ahora, el momento clave. Esta
DoubleCheck
es la misma instancia deDoubleCheck
que se utilizó para la creación deViewModel
ejemplo, cuandoFirstFragment
se le preguntó por ello. ¿Por qué es la misma instancia? Porque has aplicado un alcance al método del proveedor.El
if (result == UNINITIALIZED)
(// 1) se evalúa como falso y exactamente la misma instancia deViewModel
se devuelve al llamador -SecondFragment
.Ahora, ambos fragmentos están utilizando la misma instancia,
ViewModel
por lo tanto, está perfectamente bien que muestren los mismos datos.fuente
ViewModel
se crea con el ciclo de vida de la actividad / fragmento y se destruye tan pronto como se destruye su ciclo de vida. No debe administrar el ciclo de vida / destrucción de creación de ViewModel por sí mismo, eso es lo que los componentes de arquitectura están haciendo por usted como cliente de esa API.Ambos fragmentos reciben la actualización de livedata porque el visor mantiene ambos fragmentos en estado reanudado. Dado que necesita la actualización solo en el fragmento actual visible en el visor, la actividad del host define el contexto del fragmento actual , la actividad debe dirigir explícitamente las actualizaciones al fragmento deseado.
Debe mantener un mapa de Fragmento a LiveData que contenga entradas para todos los fragmentos (asegúrese de tener un identificador que pueda diferenciar dos instancias de fragmento del mismo fragmento) agregado al visor.
Ahora la actividad tendrá un MediatorLiveData observando los livedata originales observados directamente por los fragmentos. Cada vez que el livedata original publique una actualización, se entregará a mediatorLivedata y el mediatorlivedata en turen solo publicará el valor en livedata del fragmento seleccionado actual. Estos datos en vivo se recuperarán del mapa de arriba.
El código impl se vería así:
fuente
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
entonces, ¿cómo es que el visor mantiene ambos fragmentos en estado reanudado? Esto no estaba sucediendo antes de agregar la daga 2 al proyecto.