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.
- Actualmente hay 0 datos desechados .
-
MainFragment
es el fragmento activo actual.TrashFragment
no está creado todavía. -
MainFragment
agregó 1 datosMainFragment
. - Ahora, hay 1 datos basura
- Utilizamos el cajón de navegación para reemplazar
MainFragment
conTrashFragment
. - El observador de
onChanged
primero recibiráonChanged
, con 0 datosonChanged
- Nuevamente, el observador de
onChanged
recibirá en segundo lugaronChanged
, con 1 datosonChanged
.
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:
- Asegúrese de que REPRODUCE_BUG esté establecido en true en MainFragment
- Instala la aplicación
- Haga clic en el botón "Agregar nota destrozada"
- Cambiar a papelera
- Tenga en cuenta que solo había un formulario de notificación LiveData con el valor correcto
- Cambiar a MainFragment
- Haga clic en el botón "Agregar nota destrozada"
- Cambiar a papelera
- 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
enFragment
-
Activity
enActivity
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