Cómo usar Dagger 2 para inyectar ViewModel de los mismos fragmentos dentro de ViewPager

10

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

Estoy vinculando mi ViewModelasí:

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

Estoy usando @ContributesAndroidInjectorpara mi fragmento así:

@Module
abstract class MainFragmentBuildersModule {

    @ContributesAndroidInjector
    abstract fun contributeActivityFragment(): ActivityFragment
}

Y estoy agregando estos módulos a mi MainActivitysubcomponente 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 DaggerFragmente inyectando ViewModelProviderFactoryasí:

@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 keyserá 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

masterBranch 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.

voz silenciada
fuente
No entiendo ¿por qué no usarás el enfoque de la working_finerama? ¿Por qué necesitas el alcance?
azizbekian
@azizbekian Actualmente estoy usando la rama de trabajo bien ... pero quiero saber, por qué usar el alcance rompería esto.
hushed_voice

Respuestas:

1

Quiero recapitular la pregunta original, aquí está:

Actualmente estoy usando el trabajo fine_branch, pero quiero saber por qué usar el alcance rompería esto.

Según tengo entendido, usted tiene la impresión de que solo porque está tratando de obtener una instancia de ViewModeluso de diferentes claves, entonces debería recibir diferentes instancias de ViewModel:

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

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

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:

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

Vamos a sumergirnos y entender por qué sucede esto.

ViewModelProvider#get()Intentará obtener internamente una instancia de PagerItemViewModela ViewModelStoreque básicamente es un mapa de Stringa ViewModel.

Cuando FirstFragmentsolicita una instancia de PagerItemViewModella mapestá vacía, por mFactory.create(modelClass)lo tanto, se ejecuta, lo que termina en ViewModelProviderFactory. creator.get()termina llamando DoubleCheckcon el siguiente código:

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

El instancees ahora null, por lo tanto, PagerItemViewModelse crea una nueva instancia de y se guarda en instance(ver // 2).

Ahora ocurre exactamente el mismo procedimiento para SecondFragment:

  • fragmento pide una instancia de PagerItemViewModel
  • mapahora no está vacío, pero no contiene una instancia de PagerItemViewModelcon clavefalse
  • PagerItemViewModelse inicia una nueva instancia de a través demFactory.create(modelClass)
  • La ViewModelProviderFactoryejecución interna alcanza creator.get()cuya implementación esDoubleCheck

Ahora, el momento clave. Esta DoubleCheckes la misma instancia de DoubleCheckque se utilizó para la creación de ViewModelejemplo, cuando FirstFragmentse 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 de ViewModelse devuelve al llamador - SecondFragment.

Ahora, ambos fragmentos están utilizando la misma instancia, ViewModelpor lo tanto, está perfectamente bien que muestren los mismos datos.

azizbekian
fuente
Gracias por la respuesta. Esto tiene sentido. ¿Pero no hay una manera de arreglar esto mientras se usa el alcance?
hushed_voice
Esa fue mi pregunta anteriormente: ¿por qué necesita usar el alcance? Es como si quisieras usar un automóvil cuando escalas una montaña y ahora estás diciendo "está bien, entiendo por qué no puedo usar un automóvil, pero ¿cómo puedo usar un automóvil para escalar una montaña?" Sus intenciones no son obvias, por favor aclare.
azizbekian
Tal vez yo estoy equivocado. Mi expectativa era que usar el alcance es un mejor enfoque. Por ej. Si hay 2 actividades en mi aplicación (Inicio de sesión y Principal), usar 1 ámbito personalizado para iniciar sesión y 1 ámbito personalizado para principal, eliminará las instancias innecesarias mientras una actividad está activa
hushed_voice
> Mi expectativa era que usar el alcance es un mejor enfoque No es que uno sea mejor que el otro. Están resolviendo diferentes problemas, cada uno tiene su caso de uso.
azizbekian
> eliminará las instancias innecesarias mientras una actividad está activa. No se puede ver de dónde deben crearse esas "instancias innecesarias". ViewModelse 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.
azizbekian
0

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í:

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
fuente
Gracias por la respuesta. Estoy usando, 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.
hushed_voice
Intentaré agregar un proyecto de muestra con dicho comportamiento
hushed_voice
Hola, he agregado un proyecto de muestra. ¿Puedes por favor comprobarlo? También agregaré una recompensa por esto. (Perdón por el retraso)
hushed_voice
@hushed_voice Seguro que te responderemos.
Vishal Arora