studio oncreateloader getloadermanager example android android-loadermanager android-cursorloader asynctaskloader android-loader

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.

  1. Supongamos que el CursorLoader ha terminado de cargarse y devuelve un Cursor que ahora tiene 5 filas.
  2. El Adapter comienza a mostrar estas filas. Mueve el Cursor a la siguiente posición y llama a getView()
  3. 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.
  4. Aquí es donde está el problema : el CursorAdapter ha movido el Cursor a una posición que corresponde a una fila eliminada. El método bindView() aún intenta acceder a las columnas de esta fila usando este Cursor , 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 de ListView incluso mientras está en progreso y le pido que use el Cursor nuevo (devuelto a través de Loader#onContentChanged() y Adapter#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:

  1. El Fragment o el Adapter no deben funcionar directamente en el Loader .
  2. El Loader debe supervisar todos los cambios en los datos y solo debe proporcionar al Adapter el nuevo Cursor en onLoadFinished() 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:

  1. Utilicé el código para CursorLoader y solo modifiqué una sola línea que devuelve el Cursor . Aquí, reemplacé la llamada a ContentProvider con mi código DbManager (que a su vez utiliza DatabaseHelper para realizar una consulta y devolver el Cursor ).

    Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();

  2. 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 un Service en segundo plano y, en un par de casos, de una Activity . Utilizo directamente mi clase DbManager 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:

  1. 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 su Loader directamente. Después de la primera llamada a initLoader , el LoaderManager tiene control total sobre el Loader y lo "administrará" llamando a sus métodos en segundo plano. Debe tener mucho cuidado al llamar directamente a los métodos del Loader en este caso, ya que podría interferir con la administración subyacente del Loader . No puedo decir con seguridad que sus llamadas a onContentChanged() 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 a mLoader ). A su ListFragment 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 en onLoadFinished cuando esté disponible.

  2. Tampoco debe llamar a mAdapter.notifyDataSetChanged() en onLoadFinished . 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)