Datos fuera de sincronización entre un CursorLoader personalizado y un CursorAdapter respaldando un ListView
Fondo:
Tengo un CursorLoader
encargo que trabaja directamente con la base de datos de SQLite en vez de utilizar un ContentProvider
. Este cargador funciona con un ListFragment
respaldado por un CursorAdapter
. Hasta aquí todo bien.
Para simplificar las cosas, vamos a suponer que hay un botón Eliminar en la interfaz de usuario. Cuando el usuario hace clic en esto, onContentChanged()
una fila del DB, y también llamo onContentChanged()
en mi cargador. Además, en la devolución de llamada notifyDatasetChanged()
, llamo notifyDatasetChanged()
en mi adaptador para actualizar la interfaz de usuario.
- Actualizar la barra de progreso de AsyncTaskLoader?
- ¿Cómo funciona la cancelación de un AsyncTaskLoader?
- AsyncTaskLoader personalizado con caché
- AsyncTaskLoader no se ejecuta
- Explique AsyncTaskLoader
Problema:
Cuando los comandos de eliminación ocurren en sucesión rápida, lo que significa que onContentChanged()
se llama en sucesión rápida, bindView()
termina trabajando con datos obsoletos . Lo que esto significa es que se ha eliminado una fila, pero ListView sigue intentando mostrar esa fila. Esto conduce a las excepciones Cursor.
¿Qué estoy haciendo mal?
Código:
Este es un CursorLoader personalizado (basado en este consejo de la Sra. Diane Hackborn)
/** * An implementation of CursorLoader that works directly with SQLite database * cursors, and does not require a ContentProvider. * */ public class VideoSqliteCursorLoader extends CursorLoader { /* * This field is private in the parent class. Hence, redefining it here. */ ForceLoadContentObserver mObserver; public VideoSqliteCursorLoader(Context context) { super(context); mObserver = new ForceLoadContentObserver(); } public VideoSqliteCursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { super(context, uri, projection, selection, selectionArgs, sortOrder); mObserver = new ForceLoadContentObserver(); } /* * Main logic to load data in the background. Parent class uses a * ContentProvider to do this. We use DbManager instead. * * (non-Javadoc) * * @see android.support.v4.content.CursorLoader#loadInBackground() */ @Override public Cursor loadInBackground() { Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); if (cursor != null) { // Ensure the cursor window is filled int count = cursor.getCount(); registerObserver(cursor, mObserver); } return cursor; } /* * This mirrors the registerContentObserver method from the parent class. We * cannot use that method directly since it is not visible here. * * Hence we just copy over the implementation from the parent class and * rename the method. */ void registerObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } }
Un fragmento de mi clase ListFragment
que muestra las LoaderManager
llamada LoaderManager
; Así como un método refresh()
que llamo cuando el usuario añade / elimina un registro.
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mListView = getListView(); /* * Initialize the Loader */ mLoader = getLoaderManager().initLoader(LOADER_ID, null, this); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new VideoSqliteCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mAdapter.swapCursor(data); mAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } public void refresh() { mLoader.onContentChanged(); }
Mi CursorAdapter
es solo uno regular con newView()
siendo over-ridden para devolver el diseño de fila recién inflado XML y bindView()
usando el Cursor
para enlazar columnas a View
s en el diseño de fila.
EDITAR 1
Después de cavar en esto un poco, creo que la cuestión fundamental aquí es la forma en que CursorAdapter
maneja el Cursor
subyacente. Estoy tratando de entender cómo funciona.
Tome el siguiente escenario para una mejor comprensión.
- Supongamos que
CursorLoader
ha terminado de cargar y devuelve unCursor
que ahora tiene 5 filas. - El
Adapter
comienza a mostrar estas filas. Mueve elCursor
a la siguiente posición y llama agetView()
- En este punto, incluso mientras la vista de lista está en proceso de ser procesada, una fila (por ejemplo, con _id = 2) se elimina de la base de datos.
- Aquí es donde el problema es – El
CursorAdapter
ha movido elCursor
a una posición que corresponde a una fila eliminada. El métodobindView()
todavía intenta acceder a las columnas de esta fila utilizando esteCursor
, que no es válido y obtenemos excepciones.
Pregunta:
- ¿Es correcto este entendimiento? Estoy particularmente interesado en el punto 4 anterior en el que estoy haciendo la suposición de que cuando se elimina una fila, el
Cursor
no se actualiza a menos que yo pida que sea. - Suponiendo que esto es correcto, ¿cómo puedo pedir a mi
CursorAdapter
para descartar / abortar su representación de laListView
incluso como está en progreso y pedirle que utilice el nuevoCursor
(devuelto a través deLoader#onContentChanged()
yAdapter#notifyDatasetChanged()
?
PS Pregunta a los moderadores: ¿Debería cambiar esta edición a otra pregunta?
EDIT 2
Basado en la sugerencia de varias respuestas, parece que hubo un error fundamental en mi comprensión de cómo Loader
s trabajo. Resulta que:
- El
Fragment
o elAdapter
no deben estar funcionando directamente en elLoader
. - El
Loader
debe supervisar todos los cambios en los datos y sólo debe dar alAdapter
el nuevoCursor
enonLoadFinished()
siempre que cambie los datos.
Armado con esta comprensión, intenté los cambios siguientes. – Ninguna operación en el Loader
. El método de actualización no hace nada ahora.
Además, para depurar lo que está pasando dentro de Loader
y el ContentObserver
, me ocurrió lo siguiente:
public class VideoSqliteCursorLoader extends CursorLoader { private static final String LOG_TAG = "CursorLoader"; //protected Cursor mCursor; public final class CustomForceLoadContentObserver extends ContentObserver { private final String LOG_TAG = "ContentObserver"; public CustomForceLoadContentObserver() { super(new Handler()); } @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { Utils.logDebug(LOG_TAG, "onChange called; selfChange = "+selfChange); onContentChanged(); } } /* * This field is private in the parent class. Hence, redefining it here. */ CustomForceLoadContentObserver mObserver; public VideoSqliteCursorLoader(Context context) { super(context); mObserver = new CustomForceLoadContentObserver(); } /* * Main logic to load data in the background. Parent class uses a * ContentProvider to do this. We use DbManager instead. * * (non-Javadoc) * * @see android.support.v4.content.CursorLoader#loadInBackground() */ @Override public Cursor loadInBackground() { Utils.logDebug(LOG_TAG, "loadInBackground called"); Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); //mCursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); if (cursor != null) { // Ensure the cursor window is filled int count = cursor.getCount(); Utils.logDebug(LOG_TAG, "Count = " + count); registerObserver(cursor, mObserver); } return cursor; } /* * This mirrors the registerContentObserver method from the parent class. We * cannot use that method directly since it is not visible here. * * Hence we just copy over the implementation from the parent class and * rename the method. */ void registerObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } /* * A bunch of methods being overridden just for debugging purpose. * We simply include a logging statement and call through to super implementation * */ @Override public void forceLoad() { Utils.logDebug(LOG_TAG, "forceLoad called"); super.forceLoad(); } @Override protected void onForceLoad() { Utils.logDebug(LOG_TAG, "onForceLoad called"); super.onForceLoad(); } @Override public void onContentChanged() { Utils.logDebug(LOG_TAG, "onContentChanged called"); super.onContentChanged(); } }
Y aquí hay fragmentos de mi Fragment
y LoaderCallback
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mListView = getListView(); /* * Initialize the Loader */ getLoaderManager().initLoader(LOADER_ID, null, this); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new VideoSqliteCursorLoader(getActivity()); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { Utils.logDebug(LOG_TAG, "onLoadFinished()"); mAdapter.swapCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } public void refresh() { Utils.logDebug(LOG_TAG, "CamerasListFragment.refresh() called"); //mLoader.onContentChanged(); }
Ahora, siempre que hay un cambio en el DB (fila agregada / suprimida), el método onChange()
del ContentObserver
debe ser llamado – correcto? No veo que esto suceda. Mi ListView
nunca muestra ningún cambio. La única vez que veo algún cambio es si llamo explícitamente onContentChanged()
al Loader
.
¿Qué pasa aquí?
EDIT 3
Ok, así que volví a escribir mi Loader
para extender directamente desde AsyncTaskLoader
. Aún no veo mis cambios de base de datos actualizados, ni el método onContentChanged()
de mi Loader
se llama cuando inserto / elimino una fila en el DB 🙁
Sólo para aclarar algunas cosas:
-
He utilizado el código para
CursorLoader
y acaba de modificar una sola línea que devuelve elCursor
. Aquí, he reemplazado la llamada aContentProvider
con mi códigoDbManager
(que a su vez utilizaDatabaseHelper
para realizar una consulta y devolver elCursor
).Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();
-
Mis inserciones / actualizaciones / eliminaciones en la base de datos suceden desde cualquier lugar y no a través de
Loader
. En la mayoría de los casos las operaciones de DB están ocurriendo en unService
fondo, y en un par de casos, de unaActivity
. Utilizo directamente mi clase deDbManager
para realizar estas operaciones.
Lo que todavía no obtengo es – ¿ quién le dice a mi Loader
que una fila ha sido añadida / eliminada / modificada? En otras palabras, ¿dónde se ForceLoadContentObserver#onChange()
? En mi Cargador, inscribo a mi observador en el Cursor
:
void registerContentObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); }
Esto implicaría que la responsabilidad está en el Cursor
para notificar mObserver
cuando ha cambiado. Pero, a continuación, AFAIK, un 'Cursor' no es un objeto "en vivo" que actualiza los datos que está apuntando a como y cuando los datos se modifican en el DB.
Esta es la última iteración de mi cargador:
import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.support.v4.content.AsyncTaskLoader; public class VideoSqliteCursorLoader extends AsyncTaskLoader<Cursor> { private static final String LOG_TAG = "CursorLoader"; final ForceLoadContentObserver mObserver; Cursor mCursor; /* Runs on a worker thread */ @Override public Cursor loadInBackground() { Utils.logDebug(LOG_TAG , "loadInBackground()"); Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras(); if (cursor != null) { // Ensure the cursor window is filled int count = cursor.getCount(); Utils.logDebug(LOG_TAG , "Cursor count = "+count); registerContentObserver(cursor, mObserver); } return cursor; } void registerContentObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } /* Runs on the UI thread */ @Override public void deliverResult(Cursor cursor) { Utils.logDebug(LOG_TAG, "deliverResult()"); if (isReset()) { // An async query came in while the loader is stopped if (cursor != null) { cursor.close(); } return; } Cursor oldCursor = mCursor; mCursor = cursor; if (isStarted()) { super.deliverResult(cursor); } if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { oldCursor.close(); } } /** * Creates an empty CursorLoader. */ public VideoSqliteCursorLoader(Context context) { super(context); mObserver = new ForceLoadContentObserver(); } @Override protected void onStartLoading() { Utils.logDebug(LOG_TAG, "onStartLoading()"); if (mCursor != null) { deliverResult(mCursor); } if (takeContentChanged() || mCursor == null) { forceLoad(); } } /** * Must be called from the UI thread */ @Override protected void onStopLoading() { Utils.logDebug(LOG_TAG, "onStopLoading()"); // Attempt to cancel the current load task if possible. cancelLoad(); } @Override public void onCanceled(Cursor cursor) { Utils.logDebug(LOG_TAG, "onCanceled()"); if (cursor != null && !cursor.isClosed()) { cursor.close(); } } @Override protected void onReset() { Utils.logDebug(LOG_TAG, "onReset()"); super.onReset(); // Ensure the loader is stopped onStopLoading(); if (mCursor != null && !mCursor.isClosed()) { mCursor.close(); } mCursor = null; } @Override public void onContentChanged() { Utils.logDebug(LOG_TAG, "onContentChanged()"); super.onContentChanged(); } }
- AsyncTaskLoader no llama a onLoadFinished a menos que devuelva un nuevo objeto en loadInBackground
- Prueba de AsyncTaskLoaders con Robolectric
- AsyncTaskLoader onLoadFinished con un cambio de tarea y configuración pendiente
- Cómo manejar los errores en AsyncTaskLoader personalizado?
- Volley o ASyncTaskLoader
- Utilizar LoaderCallback sin Fragmento
- Cómo pasar params a Android AsnycTaskLoader
- Actualizar la interfaz de usuario desde un AsyncTaskLoader
No estoy 100% seguro basado en el código que has proporcionado, pero un par de cosas sobresalen:
-
Lo primero que sobresale es que has incluido este método en tu
ListFragment
:public void refresh() { mLoader.onContentChanged(); }
Cuando se utiliza
LoaderManager
, rara vez es necesario (ya menudo es peligroso) manipular directamente aLoader
. Después de la primera llamada ainitLoader
,LoaderManager
tiene control total sobre elLoader
y lo "administrará" llamando a sus métodos en segundo plano. Debe tener mucho cuidado al llamar a los métodos deLoader
directamente en este caso, ya que podría interferir con la gestión subyacente delLoader
. No puedo asegurar que tus llamadas aonContentChanged()
son incorrectas, ya que no las mencionas en tu mensaje, pero no debería ser necesario en tu situación (y tampoco debería tener una referencia amLoader
). SuListFragment
no le importa cómo se detectan los cambios … ni le importa cómo se cargan los datos. Todo lo que sabe es que los nuevos datos mágicamente se proporcionarán enonLoadFinished
cuando esté disponible. -
Tampoco debería llamar a
mAdapter.notifyDataSetChanged()
enonLoadFinished
.swapCursor
hará esto por usted.
En su mayor parte, el marco de Loader
debe hacer todas las cosas complicadas que implican cargar datos y administrar el Cursor
. Su código ListFragment
debe ser simple en comparación.
Editar # 1:
Por lo que puedo decir, el CursorLoader
basa en el ForceLoadContentObserver
(una clase interna anidada proporcionada en la implementación de Loader<D>
), por lo que parece que el problema aquí es que está implementando su personalizado ContentObserver
, pero no hay nada Establecido para reconocerlo.Muchas de las cosas de "auto-notificación" se realizan en la implementación de Loader<D>
y AsyncTaskLoader<D>
y, por lo tanto, están ocultas del Loader
concreto (como CursorLoader
) No tiene idea acerca de CustomForceLoadContentObserver
, así que ¿por qué debería recibir alguna notificación?).
Usted mencionó en su publicación actualizada que no puede acceder a final ForceLoadContentObserver mObserver;
Directamente, ya que es un campo oculto. Su solución era implementar su propio ContentObserver
personalizado y llamar a registerObserver()
en su método loadInBackground
(que hará que registerContentObserver
sea llamado en su Cursor
). Esta es la razón por la que no recibe notificaciones … porque ha utilizado un ContentObserver
personalizado que nunca ha sido reconocido por el marco de ContentObserver
.
Para solucionar el problema, debe tener su clase extend AsyncTaskLoader<Cursor>
directamente extend AsyncTaskLoader<Cursor>
lugar de CursorLoader
(es decir, simplemente copie y pegue las partes que está heredando de CursorLoader
en su clase). De esta manera no se ejecutará en ningún problema con el campo ocultado ForceLoadContentObserver
.
Editar # 2:
De acuerdo con Commonsware , no hay una manera fácil de configurar notificaciones globales procedentes de una base de datos SQLiteDatabase
, por lo que SQLiteCursorLoader
en su biblioteca Loaderex
basa en el Loader
llama onContentChanged()
sobre sí mismo cada vez que se realiza una transacción. La forma más fácil de difundir las notificaciones directamente desde la fuente de datos es implementar un ContentProvider
y utilizar un CursorLoader
. De esta manera, puede confiar en que las notificaciones se difundirán en su CursorLoader
cada vez que su Service
actualice el origen de datos subyacente.
No dudo que hay otras soluciones (es decir, quizás mediante la creación de un ContentObserver
global … o tal vez incluso utilizando el método ContentResolver#notifyChange
sin un ContentProvider
), pero la solución más limpia y más simple parece ser simplemente implementar un Privado ContentProvider
.
(Ps, asegúrate de establecer android:export="false"
en la etiqueta del proveedor en tu manifiesto para que tu ContentProvider
no pueda ser visto por otras aplicaciones !: p)
Esto no es realmente una solución para su problema, pero puede ser útil para usted:
Hay un método CursorLoader.setUpdateThrottle(long delayMS)
, que impone un tiempo mínimo entre completar loadInBackground y la próxima carga que se está programando.
Alternativa:
Siento que usar un CursoLoader es demasiado pesado para esta tarea. Lo que necesita estar en sincronía es la base de datos add / delete, y se puede hacer en un método sincronizado. Como dije en un comentario anterior, cuando un servicio mDNS se detiene, quite de él db (de forma sincronizada), envíe remove broadcast, en el receptor: quite de la lista de data-holder y notifique. Eso debería ser suficiente. Sólo para evitar el uso de un arraylist extra (para respaldar el adaptador), utilizando un CursorLoader es extra-trabajo.
Debe realizar alguna sincronización en el objeto ListFragment
.
La llamada a notifyDatasetChanged()
debe estar sincronizada.
synchronized(this) { // this is ListFragment or ListView. notifyDatasetChanged(); }
He leído todo su hilo como yo estaba teniendo el mismo problema, la siguiente declaración es lo que resolvió este problema para mí:
getLoaderManager().restartLoader(0, null, this);
A tenía el mismo problema. Lo resolví por:
@Override public void onResume() { super.onResume(); // Always call the superclass method first if (some_condition) { getSupportLoaderManager().getLoader(LOADER_ID).onContentChanged(); } }
- ¿Cómo puedo INTERCEPTAR un SMS entrante con un texto específico
- Android – Notificación incorrecta publicada – No se pudo expandir RemoteViews para: StatusBarNotification