ventajas tipos taylor segun sampieri participante observación observacion malinowski desventajas bogdan autores android android-architecture-components android-architecture-livedata

android - taylor - tipos de observacion



¿Por qué el observador LiveData se activa dos veces para un observador recién conectado? (7)

No es un error, es una característica. ¡Lee por qué!

El método de observadores void onChanged(@Nullable T t) se llama dos veces. Esta bien.

La primera vez que se llama en el inicio. La segunda vez se llama tan pronto como Room ha cargado los datos. Por lo tanto, en la primera llamada, el objeto LiveData todavía está vacío. Está diseñado de esta manera por buenas razones.

Segunda llamada

Comencemos con la segunda llamada, su punto 7. La documentación de Room dice:

Room genera todo el código necesario para actualizar el objeto LiveData cuando se actualiza una base de datos. El código generado ejecuta la consulta de forma asíncrona en un subproceso en segundo plano cuando es necesario.

El código generado es un objeto de la clase ComputableLiveData mencionado en otras publicaciones. Gestiona un objeto MutableLiveData . Sobre este objeto LiveData , llama a LiveData::postValue(T value) que luego llama a LiveData::setValue(T value) .

LiveData::setValue(T value) llama a LiveData::dispatchingValue(@Nullable ObserverWrapper initiator) . Esto llama a LiveData::considerNotify(ObserverWrapper observer) con el envoltorio de observador como parámetro. Esto finalmente llama onChanged() al observador con los datos cargados como parámetro.

Primera llamada

Ahora para la primera llamada, su punto 6.

Establece sus observadores dentro del onCreateView() . Después de este punto, el ciclo de vida cambia su estado dos veces para que sea visible, on start y on resume . La clase interna LiveData::LifecycleBoundObserver se notifica sobre dichos cambios de estado porque implementa la interfaz GenericLifecycleObserver , que contiene un método llamado void onStateChanged(LifecycleOwner source, Lifecycle.Event event); .

Este método llama ObserverWrapper::activeStateChanged(boolean newActive) medida que LifecycleBoundObserver extiende ObserverWrapper . El método activeStateChanged llama a dispatchingValue() que a su vez llama a LiveData::considerNotify(ObserverWrapper observer) con el envoltorio del observador como parámetro. Esto finalmente llama onChanged() sobre el observador.

Todo esto sucede bajo ciertas condiciones. Admito que no investigué todas las condiciones dentro de la cadena de métodos. Hay dos cambios de estado, pero onChanged() solo se activa una vez, porque las condiciones verifican este tipo de cosas.

La conclusión aquí es que hay una cadena de métodos que se activan ante los cambios del ciclo de vida. Este es el responsable de la primera llamada.

Línea de fondo

Creo que nada va mal con tu código. Está bien, que el observador es llamado a la creación. Así se puede llenar con los datos iniciales del modelo de vista. Eso es lo que debe hacer un observador, incluso si la parte de la base de datos del modelo de vista aún está vacía en la primera notificación.

Uso

La primera notificación básicamente indica que el modelo de vista está listo para mostrarse, a pesar de que todavía no está cargado con datos de bases de datos subyacentes. La segunda notificación dice, que esta información está lista.

Cuando piensas en conexiones de db lentas, este es un enfoque razonable. Es posible que desee recuperar y mostrar otros datos del modelo de vista activado por la notificación, que no proviene de la base de datos.

Android tiene una guía de cómo lidiar con la carga lenta de la base de datos. Sugieren utilizar marcadores de posición. En este ejemplo, la brecha es tan breve, que no hay razón para ir a tal extensión.

Apéndice

Ambos fragmentos usan sus propios objetos ComputableLiveData , por eso el segundo objeto no está precargado desde el primer fragmento.

Piensa también en el caso de la rotación. Los datos del modelo de vista no cambian. No activa una notificación. Los cambios de estado del ciclo de vida solo activan la notificación de la nueva vista nueva.

Mi entendimiento sobre LiveData es que activará un observador sobre el cambio de estado actual de los datos, y no una serie de cambios de estado históricos de los datos.

Actualmente, tengo un MainFragment , que realiza la operación de escritura en la Room , para cambiar los datos no eliminados , a los datos eliminados .

También otro TrashFragment , que observa a los datos de la papelera .

Considere el siguiente escenario.

  1. Actualmente hay 0 datos desechados .
  2. MainFragment es el fragmento activo actual. TrashFragment no está creado todavía.
  3. MainFragment agregó 1 datos MainFragment .
  4. Ahora, hay 1 datos basura
  5. Utilizamos el cajón de navegación para reemplazar MainFragment con TrashFragment .
  6. El observador de onChanged primero recibirá onChanged , con 0 datos onChanged
  7. Nuevamente, el observador de onChanged recibirá en segundo lugar onChanged , con 1 datos onChanged .

Lo que está fuera de mi expectativa es que el artículo (6) no debería ocurrir. TrashFragment solo debe recibir los últimos datos de la papelera , que es 1.

Aquí están mis códigos

TrashFragment.java

public class TrashFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... noteViewModel.getTrashedNotesLiveData().removeObservers(this); noteViewModel.getTrashedNotesLiveData().observe(this, notesObserver);

MainFragment.java

public class MainFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... noteViewModel.getNotesLiveData().removeObservers(this); noteViewModel.getNotesLiveData().observe(this, notesObserver);

NoteViewModel .java

public class NoteViewModel extends ViewModel { private final LiveData<List<Note>> notesLiveData; private final LiveData<List<Note>> trashedNotesLiveData; public LiveData<List<Note>> getNotesLiveData() { return notesLiveData; } public LiveData<List<Note>> getTrashedNotesLiveData() { return trashedNotesLiveData; } public NoteViewModel() { notesLiveData = NoteplusRoomDatabase.instance().noteDao().getNotes(); trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes(); } }

Código que trata de la habitación.

public enum NoteRepository { INSTANCE; public LiveData<List<Note>> getTrashedNotes() { NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao(); return noteDao.getTrashedNotes(); } public LiveData<List<Note>> getNotes() { NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao(); return noteDao.getNotes(); } } @Dao public abstract class NoteDao { @Transaction @Query("SELECT * FROM note where trashed = 0") public abstract LiveData<List<Note>> getNotes(); @Transaction @Query("SELECT * FROM note where trashed = 1") public abstract LiveData<List<Note>> getTrashedNotes(); @Insert(onConflict = OnConflictStrategy.REPLACE) public abstract long insert(Note note); } @Database( entities = {Note.class}, version = 1 ) public abstract class NoteplusRoomDatabase extends RoomDatabase { private volatile static NoteplusRoomDatabase INSTANCE; private static final String NAME = "noteplus"; public abstract NoteDao noteDao(); public static NoteplusRoomDatabase instance() { if (INSTANCE == null) { synchronized (NoteplusRoomDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder( NoteplusApplication.instance(), NoteplusRoomDatabase.class, NAME ).build(); } } } return INSTANCE; } }

¿Alguna idea de cómo puedo evitar recibir onChanged dos veces, por una misma información?

Manifestación

Creé un proyecto de demostración para demostrar este problema.

Como puede ver, después de realizar la operación de escritura (haga clic en el botón AGREGAR NOTA RETIRADA ) en MainFragment , cuando cambio a TrashFragment , espero que solo se llame una vez a onChanged en TrashFragment . Sin embargo, se está llamando dos veces.

El proyecto de demostración se puede descargar desde https://github.com/yccheok/live-data-problem


Bifurqué tu proyecto y lo probé un poco. Por todo lo que puedo decirte, descubriste un error serio.

Para facilitar la reproducción y la investigación, edité un poco su proyecto. Puede encontrar el proyecto actualizado aquí: https://github.com/techyourchance/live-data-problem . También abrí una solicitud de extracción a su repositorio.

Para asegurarme de que esto no pase desapercibido, también abrí un problema en el rastreador de problemas de Google:

Pasos para reproducir:

  1. Asegúrese de que REPRODUCE_BUG esté establecido en true en MainFragment
  2. Instala la aplicación
  3. Haga clic en el botón "Agregar nota destrozada"
  4. Cambiar a papelera
  5. Tenga en cuenta que solo había un formulario de notificación LiveData con el valor correcto
  6. Cambiar a MainFragment
  7. Haga clic en el botón "Agregar nota destrozada"
  8. Cambiar a papelera
  9. Tenga en cuenta que hubo dos notificaciones de LiveData, la primera con un valor incorrecto

Tenga en cuenta que si establece REPRODUCE_BUG en falso, el error no se reproduce. Demuestra que la suscripción a LiveData en MainFragment cambió el comportamiento en TrashFragment.

Resultado esperado: solo una notificación con el valor correcto en cualquier caso. No hay cambios en el comportamiento debido a las suscripciones anteriores.

Más información: miré las fuentes un poco y parece que las notificaciones se activan debido a la activación de LiveData y la nueva suscripción de Observer. Podría estar relacionado con la forma en que ComputableLiveData descarga en el cálculo Activo () al Ejecutor.


Cogí el tenedor de Vasiliy de tu tenedor del tenedor y realicé un poco de depuración para ver qué pasaba.

Podría estar relacionado con la forma en que ComputableLiveData descarga en el cálculo Activo () al Ejecutor.

Cerrar. La forma en que LiveData<List<T>> Room es que crea un ComputableLiveData , que realiza un seguimiento de si su conjunto de datos se ha invalidado debajo de Room.

trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();

Entonces, cuando se escribe en la tabla de note , entonces el Invalidation Tracker vinculado a LiveData llamará invalidate() cuando ocurra una escritura.

@Override public LiveData<List<Note>> getNotes() { final String _sql = "SELECT * FROM note where trashed = 0"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0); return new ComputableLiveData<List<Note>>() { private Observer _observer; @Override protected List<Note> compute() { if (_observer == null) { _observer = new Observer("note") { @Override public void onInvalidated(@NonNull Set<String> tables) { invalidate(); } }; __db.getInvalidationTracker().addWeakObserver(_observer); }

Ahora, lo que necesitamos saber es que invalidate() ComputableLiveData realmente actualizará el conjunto de datos, si LiveData está activo .

// invalidation check always happens on the main thread @VisibleForTesting final Runnable mInvalidationRunnable = new Runnable() { @MainThread @Override public void run() { boolean isActive = mLiveData.hasActiveObservers(); if (mInvalid.compareAndSet(false, true)) { if (isActive) { // <-- this check here is what''s causing you headaches mExecutor.execute(mRefreshRunnable); } } } };

Donde liveData.hasActiveObservers() es:

public boolean hasActiveObservers() { return mActiveCount > 0; }

Por refreshRunnable tanto, refreshRunnable realidad solo se ejecuta si hay un observador activo (afaik significa que el ciclo de vida al menos ha comenzado y observa los datos en vivo).

Esto significa que cuando se suscribe a TrashFragment, entonces lo que sucede es que su LiveData se almacena en la Actividad por lo que se mantiene vivo incluso cuando TrashFragment se ha ido y conserva el valor anterior.

Sin embargo, cuando abre TrashFragment, luego TrashFragment se suscribe, LiveData se activa, ComputableLiveData verifica la invalidación (lo cual es cierto ya que nunca se volvió a calcular porque los datos en vivo no estaban activos), lo calcula de forma asíncrona en el hilo de fondo, y cuando está Completo, el valor es publicado.

Entonces obtienes dos devoluciones de llamada porque:

1.) la primera llamada "onChanged" es el valor retenido previamente del LiveData mantenido vivo en el ViewModel de la actividad

2.) la segunda llamada "onChanged" es el conjunto de resultados recién evaluado de su base de datos, donde el cálculo se activó porque los datos en vivo de la Sala se activaron.

Así que técnicamente esto es por diseño. Si desea asegurarse de que solo obtiene el valor "más nuevo y más grande", debe utilizar un ViewModel con ámbito de fragmentos.

También es posible que desee comenzar a observar en onCreateView() y usar viewLifecycle para el ciclo de vida de su LiveData (esta es una nueva adición para que no necesite eliminar observadores en onDestroyView() .

Si es importante que el Fragmento vea el último valor, incluso cuando el Fragmento NO esté activo y NO lo esté observando, entonces como ViewModel tiene un ámbito de Actividad, es posible que desee registrar un observador en la Actividad para asegurarse de que haya un Observador activo en su LiveData.


Descubrí específicamente por qué está actuando como es. La conducta observada se cambió en el fragmento de basura una vez la primera vez que se activa el fragmento después de trashear una nota (en el inicio de una aplicación nueva) y se llama dos veces cuando el fragmento se activa después de que se rasca una nota.

Las llamadas dobles suceden porque:

Llamada # 1: el fragmento está en transición entre STOPPED y STARTED en su ciclo de vida y esto hace que se establezca una notificación al objeto LiveData (¡es un observador del ciclo de vida después de todo!). El código LiveData llama al controlador onChanged () porque cree que la versión de los datos del observador debe actualizarse (más sobre esto más adelante). Nota: la actualización real de los datos aún podría estar pendiente en este punto, lo que provocará que se llame al onChange () con datos obsoletos.

Llamada n. ° 2: se establece como resultado de la configuración de la configuración de LiveData (ruta normal). Nuevamente, el objeto LiveData piensa que la versión de los datos del observador es obsoleta.

Ahora, ¿por qué solo se llama a onChanged () la primera vez que se activa la vista después del inicio de la aplicación? Esto se debe a que la primera vez que se ejecuta el código de comprobación de la versión de LiveData como resultado de la transición DETENIDA-> INICIADA, los datos en vivo nunca se configuraron para nada y, por lo tanto, LiveData omite informar al observador. Las llamadas subsiguientes a través de esta ruta de código (ver considerNotify () en LiveData.java) se ejecutan después de que los datos se hayan configurado al menos una vez.

LiveData determina si el observador tiene datos obsoletos manteniendo un número de versión que indica cuántas veces se han configurado los datos. También registra el último número de versión enviado al cliente. Cuando se establecen nuevos datos, LiveData puede comparar estas versiones para determinar si una llamada onChange () está garantizada.

Aquí están los números de versión durante las llamadas al código de comprobación de la versión de LiveData para las 4 llamadas:

Ver. Last Seen Ver. of the OnChanged() by Observer LiveData Called? -------------- --------------- ----------- 1 -1 (never set) -1 (never set) N 2 -1 0 Y 3 -1 0 Y 4 0 1 Y

Si se está preguntando por qué la última versión vista por el observador en la llamada 3 es -1, aunque se llamó a onChanged () la segunda vez, es porque el observador en las llamadas 1/2 es un observador diferente al de las llamadas 3/4. (El observador está en el fragmento que fue destruido cuando el usuario regresó al fragmento principal).

Una forma fácil de evitar la confusión con respecto a las llamadas espurias que ocurren como resultado de las transiciones del ciclo de vida es mantener un indicador en el fragmento intializado a falso que indique si el fragmento se ha reanudado por completo. Establezca ese indicador en verdadero en el controlador onResume () y luego verifique si ese indicador es verdadero en su controlador onChanged (). De esa manera, puede estar seguro de que está respondiendo a los eventos que sucedieron porque los datos se establecieron realmente.


Esto es lo que pasa bajo el capó:

ViewModelProviders.of(getActivity())

A medida que usa getActivity (), esto conserva su NoteViewModel mientras el alcance de MainActivity está activo, así como su papeleraNotesLiveData.

Cuando abre por primera vez su sala de Papelera de Bases, la base de datos y su trashedNotesLiveData se llenan con el valor de la papelera (en la primera apertura solo hay una llamada onChange ()). Entonces este valor se almacena en caché en trashedNotesLiveData.

Luego llega al fragmento principal, agregue algunas notas desechadas y vuelva a TrashFragment. Esta vez, primero se le entrega el valor almacenado en caché en trashedNotesLiveData, mientras que room hace una consulta asíncrona. Cuando la consulta finaliza, se le trae el último valor. Es por esto que obtienes dos llamadas onChange ().

Entonces, la solución es que necesitas limpiar TrashedNotesLiveData antes de abrir TrashFragment. Esto puede hacerse en su método getTrashedNotesLiveData ().

public LiveData<List<Note>> getTrashedNotesLiveData() { return NoteplusRoomDatabase.instance().noteDao().getTrashedNotes(); }

O puedes usar algo como este SingleLiveEvent

O puede usar un MediatorLiveData que intercepta la sala generada y devuelve solo valores distintos.

final MediatorLiveData<T> distinctLiveData = new MediatorLiveData<>(); distinctLiveData.addSource(liveData, new Observer<T>() { private boolean initialized = false; private T lastObject = null; @Override public void onChanged(@Nullable T t) { if (!initialized) { initialized = true; lastObject = t; distinctLiveData.postValue(lastObject); } else if (t != null && !t.equals(lastObject)) { lastObject = t; distinctLiveData.postValue(lastObject); } } });


He introducido un solo cambio en su código:

noteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);

en lugar de:

noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);

en los métodos onCreate(Bundle) Fragment . Y ahora funciona perfectamente.

En su versión, obtuvo una referencia de NoteViewModel común a ambos Fragmentos (de la Actividad). ViewModel tenía Observer registrado en Fragmento anterior, creo. Por LiveData tanto, LiveData mantuvo la referencia tanto a los de Observer (en MainFragment y TrashFragment ) como a ambos valores.

Así que supongo que la conclusión podría ser que debería obtener ViewModel de ViewModelProviders de:

  • Fragment en Fragment
  • Activity en Activity

Por cierto

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

No es necesario en Fragmentos, sin embargo, yo recomendaría ponerlo en onStop .


No estoy seguro de si este problema sigue activo.

Pero el autor principal fue un error en el fragmento del propietario de LifeCycle para los fragmentos que no se eliminó cuando se destruye la vista.

Anteriormente, tendría que implementar su propio propietario de lyfecycle que movería el estado a destroyed cuando se onDestroyView .

Este ya no debería ser el caso si apuntas y compilas con al menos API 28