oncreateloader - loading android
Datos desincronizados entre un CursorLoader personalizado y un CursorAdapter respaldando un ListView (5)
Fondo:
Tengo un CursorLoader
personalizado que funciona directamente con la base de datos SQLite en lugar de usar un ContentProvider
. Este cargador funciona con un ListFragment
respaldado por un CursorAdapter
. Hasta aquí todo bien.
Para simplificar las cosas, supongamos que hay un botón Eliminar en la interfaz de usuario. Cuando el usuario hace clic en esto, onContentChanged()
una fila de la base de datos y también onContentChanged()
a onContentChanged()
en mi cargador. Además, en la devolución de llamada notifyDatasetChanged()
, llamo a notifyDatasetChanged()
en mi adaptador para actualizar la interfaz de usuario.
Problema:
Cuando los comandos de eliminación se suceden en una sucesión rápida, lo que significa que se llama a onContentChanged()
en una sucesión rápida, bindView()
termina trabajando con datos obsoletos . Lo que esto significa es que se ha eliminado una fila, pero el ListView todavía está intentando mostrar esa fila. Esto lleva a las excepciones del 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 de LoaderManager
; así como un método refresh()
que llamo cada vez que el usuario agrega / 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();
}
My CursorAdapter
es solo uno normal con newView()
está sobrepasado para devolver el XML del diseño de fila recién inflado y bindView()
usando el Cursor
para enlazar columnas con View
s en el diseño de fila.
EDITAR 1
Después de profundizar un poco en esto, creo que el problema fundamental aquí es la forma en que el CursorAdapter
maneja el Cursor
subyacente. Estoy tratando de entender cómo funciona eso.
Tome el siguiente escenario para una mejor comprensión.
- Supongamos que el
CursorLoader
ha terminado de cargarse 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 cuando la vista de lista se está procesando, se elimina una fila (por ejemplo, con _id = 2) de la base de datos.
- Aquí es donde está el problema : el
CursorAdapter
ha movido elCursor
a una posición que corresponde a una fila eliminada. El métodobindView()
aún intenta acceder a las columnas de esta fila usando esteCursor
, que no es válido y obtenemos excepciones.
Pregunta:
- ¿Es correcto este entendimiento? Estoy particularmente interesado en el punto 4 anterior, donde supongo que cuando se elimina una fila, el
Cursor
no se actualiza a menos que solicite que lo haga. - Suponiendo que esto sea correcto, ¿cómo le pido a mi
CursorAdapter
que descarte / aborte su representación deListView
incluso mientras está en progreso y le pido que use elCursor
nuevo (devuelto a través deLoader#onContentChanged()
yAdapter#notifyDatasetChanged()
) ?
Pregunta del PS a los moderadores: ¿Debería esta edición ser movida a una pregunta separada?
Editar 2
Según la sugerencia de varias respuestas, parece que hubo un error fundamental en mi comprensión de cómo funciona el Loader
. Resulta que:
- El
Fragment
o elAdapter
no deben funcionar directamente en elLoader
. - El
Loader
debe supervisar todos los cambios en los datos y solo debe proporcionar alAdapter
el nuevoCursor
enonLoadFinished()
cada vez que se modifican los datos.
Armado con este entendimiento, intenté los siguientes cambios. - Ninguna operación en el Loader
absoluto. El método de actualización no hace nada ahora.
Además, para depurar lo que sucede dentro del Loader
y el ContentObserver
, se me ocurrió esto:
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 haya un cambio en la base de datos (fila agregada / eliminada), se debe onChange()
método onChange()
del ContentObserver
: ¿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()
en el Loader
.
¿Qué está mal aquí?
EDITAR 3
Ok, entonces reescribí mi Loader
para extenderlo directamente desde AsyncTaskLoader
. Sigo sin ver cómo se actualizan los cambios en mi base de datos, ni se onContentChanged()
método onContentChanged()
de mi Loader
cuando inserto / borro una fila en la base de datos :-(
Solo para aclarar algunas cosas:
Utilicé el código para
CursorLoader
y solo modifiqué una sola línea que devuelve elCursor
. Aquí, reemplacé 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 ocurren desde otro lugar y no a través del
Loader
. En la mayoría de los casos, las operaciones de DB se realizan en unService
en segundo plano y, en un par de casos, de unaActivity
. Utilizo directamente mi claseDbManager
para realizar estas operaciones.
Lo que todavía no entiendo es: ¿ quién le dice a mi Loader
que se ha agregado / eliminado / modificado una fila? En otras palabras, ¿dónde se ForceLoadContentObserver#onChange()
? En mi cargador, registro 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 a mObserver
cuando haya cambiado. Pero, luego, AFAIK, un ''Cursor'' no es un objeto "en vivo" que actualiza los datos a los que apunta como y cuando los datos se modifican en la base de datos.
Aquí está 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();
}
}
Alternativa:
Siento que usar un CursoLoader es demasiado pesado para esta tarea. Lo que debe estar sincronizado es la base de datos agregar / eliminar, y se puede hacer en un método sincronizado. Como dije en un comentario anterior, cuando un servicio mDNS se detiene, elimínelo de la base de datos (de manera sincronizada), envíe la difusión de eliminación, en el receptor: elimine de la lista de titulares de datos y notifique. Eso debería ser suficiente. Solo para evitar utilizar un arraylist adicional (para respaldar el adaptador), usar un CursorLoader es trabajo extra.
Debes hacer alguna sincronización en el objeto ListFragment
.
La llamada a notifyDatasetChanged()
debe estar sincronizada.
synchronized(this) { // this is ListFragment or ListView.
notifyDatasetChanged();
}
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();
}
}
Esto no es realmente una solución para su problema, pero podría serle de alguna utilidad:
Existe un método CursorLoader.setUpdateThrottle(long delayMS)
, que impone un tiempo mínimo entre la finalización de loadInBackground y la próxima carga que se está programando.
Leí todo el hilo, ya que tenía el mismo problema, la siguiente declaración es lo que resolvió este problema para mí:
getLoaderManager().restartLoader(0, null, this);
No estoy 100% seguro basado en el código que has proporcionado, pero sobresalen un par de cosas:
Lo primero que sobresale es que ha incluido este método en su
ListFragment
:public void refresh() { mLoader.onContentChanged(); }
Cuando se utiliza el
LoaderManager
, rara vez es necesario (y suele ser peligroso) manipular suLoader
directamente. Después de la primera llamada ainitLoader
, elLoaderManager
tiene control total sobre elLoader
y lo "administrará" llamando a sus métodos en segundo plano. Debe tener mucho cuidado al llamar directamente a los métodos delLoader
en este caso, ya que podría interferir con la administración subyacente delLoader
. No puedo decir con seguridad que sus llamadas aonContentChanged()
sean incorrectas, ya que no lo menciona en su publicación, pero no debería ser necesario en su situación (y tampoco debe contener una referencia amLoader
). A 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 se proporcionarán mágicamente enonLoadFinished
cuando esté disponible.Tampoco debe llamar a
mAdapter.notifyDataSetChanged()
enonLoadFinished
.swapCursor
hará esto por ti.
En su mayor parte, el marco del Loader
debe hacer todas las cosas complicadas que involucran la carga de datos y la administración de los Cursor
. Su código ListFragment
debe ser simple en comparación.
Edición # 1:
Por lo que puedo decir, el CursorLoader
basa en el ForceLoadContentObserver
(una clase interna anidada proporcionada en la implementación del Loader<D>
) ... por lo que parece que el problema aquí es que está implementando su ContentObserver
personalizado, pero nada es configurar para reconocerlo.Una gran cantidad de "autodeclaración" se realiza en la implementación del Loader<D>
y AsyncTaskLoader<D>
y, por lo tanto, está oculta de los Loader
s concretos (como CursorLoader
) que hacen el trabajo real (es decir, Loader<D>
no tiene idea acerca de CustomForceLoadContentObserver
, ¿por qué debería recibir alguna notificación?
Usted mencionó en su publicación actualizada que no puede acceder al final ForceLoadContentObserver mObserver;
directamente, ya que es un campo oculto. Su solución fue implementar su propio ContentObserver
personalizado y llamar a registerObserver()
en su método loadInBackground
(lo que hará que se llame a registerContentObserver
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 del Loader
.
Para solucionar el problema, debe hacer que 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 tendrá problemas con el campo oculto ForceLoadContentObserver
.
Edición # 2:
De acuerdo con Commonsware , no hay una manera fácil de configurar notificaciones globales provenientes de una base de datos SQLiteDatabase
, por lo que el SQLiteCursorLoader
en su biblioteca Loaderex
basa en el Loader
que onContentChanged()
en sí mismo cada vez que se realiza una transacción. La forma más fácil de transmitir notificaciones directamente desde el origen de datos es implementar un ContentProvider
y usar un CursorLoader
. De esta manera, puede confiar en que las notificaciones se transmitirán a su CursorLoader
cada vez que su Service
actualice la fuente de datos subyacente.
No dudo que haya otras soluciones (es decir, tal vez configurando un ContentObserver
global ... o tal vez incluso usando el método ContentResolver#notifyChange
sin un ContentProvider
), pero la solución más limpia y simple parece ser simplemente implementar un ContentProvider
privado.
(ps, asegúrate de configurar android:export="false"
en la etiqueta del proveedor en tu manifiesto para que otras aplicaciones no vean tu ContentProvider
: p)