Uso de CursorLoader sin ContentProvider

107

La documentación del SDK de Android dice que el startManagingCursor()método está depravado:

Este método está en desuso. Utilice la nueva clase CursorLoader con LoaderManager en su lugar; esto también está disponible en plataformas más antiguas a través del paquete de compatibilidad de Android. Este método permite que la actividad se encargue de administrar el ciclo de vida del Cursor dado por usted en función del ciclo de vida de la actividad. Es decir, cuando la actividad se detiene, llamará automáticamente a deactivate () en el Cursor dado, y cuando se reinicie más tarde, llamará a requery () por usted. Cuando se destruye la actividad, todos los cursores administrados se cerrarán automáticamente. Si está apuntando a HONEYCOMB o posterior, considere usar LoaderManager en su lugar, disponible a través de getLoaderManager ()

Entonces me gustaría usar CursorLoader. Pero, ¿cómo puedo usarlo con personalizado CursorAdaptery sin ContentProvider, cuando necesito URI en el constructor de CursorLoader?

sealskej
fuente
@Alex Lockwood, por qué estamos usando CursorAdapter sin ContentProvider,
por qué estamos usando CursorAdapter sin ContentProvider, por favor sugiera stackoverflow.com/questions/20419278/…

Respuestas:

155

Escribí un CursorLoader simple que no necesita un proveedor de contenido:

import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;

/**
 * Used to write apps that run on platforms prior to Android 3.0. When running
 * on Android 3.0 or above, this implementation is still used; it does not try
 * to switch to the framework's implementation. See the framework SDK
 * documentation for a class overview.
 *
 * This was based on the CursorLoader class
 */
public abstract class SimpleCursorLoader extends AsyncTaskLoader<Cursor> {
    private Cursor mCursor;

    public SimpleCursorLoader(Context context) {
        super(context);
    }

    /* Runs on a worker thread */
    @Override
    public abstract Cursor loadInBackground();

    /* Runs on the UI thread */
    @Override
    public void deliverResult(Cursor cursor) {
        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();
        }
    }

    /**
     * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
     * will be called on the UI thread. If a previous load has been completed and is still valid
     * the result may be passed to the callbacks immediately.
     * <p/>
     * Must be called from the UI thread
     */
    @Override
    protected void onStartLoading() {
        if (mCursor != null) {
            deliverResult(mCursor);
        }
        if (takeContentChanged() || mCursor == null) {
            forceLoad();
        }
    }

    /**
     * Must be called from the UI thread
     */
    @Override
    protected void onStopLoading() {
        // Attempt to cancel the current load task if possible.
        cancelLoad();
    }

    @Override
    public void onCanceled(Cursor cursor) {
        if (cursor != null && !cursor.isClosed()) {
            cursor.close();
        }
    }

    @Override
    protected void onReset() {
        super.onReset();

        // Ensure the loader is stopped
        onStopLoading();

        if (mCursor != null && !mCursor.isClosed()) {
            mCursor.close();
        }
        mCursor = null;
    }
}

Solo necesita la AsyncTaskLoaderclase. O el de Android 3.0 o superior, o el que viene con el paquete de compatibilidad.

También escribí unListLoader que es compatible con LoadManagery se usa para recuperar una java.util.Listcolección genérica .

Cristian
fuente
13
Encontré un buen ejemplo de código que usa esto - bitbucket.org/ssutee/418496_mobileapp/src/fc5ee705a2fd/demo/… - ¡lo encontré muy útil!
Shushu
@Cristian Gracias por el ejemplo. ¿Cuál es la licencia asociada con su clase? ¿Cómo se puede reutilizar?
codinguser
2
La licencia es Apache 2.0; puedes reutilizarlo donde y cuando quieras. Avísame si tienes alguna mejora.
Cristian
14
¡Buena cosa! Los usuarios deben tener en cuenta una limitación, que es que no tiene ningún mecanismo para actualizar los cambios de datos (como se supone que deben hacer los
cargadores
1
@Jadeye aquí tienes man: ListLoader y SupportListLoader
Cristian
23

Escriba su propio cargador que use su clase de base de datos en lugar de un proveedor de contenido. La forma más fácil es simplemente tomar la fuente de la CursorLoaderclase de la biblioteca de compatibilidad y reemplazar las consultas del proveedor con consultas a su propia clase auxiliar de base de datos.

Nikolay Elenkov
fuente
1
Esta es la forma más fácil en mi opinión. En mi aplicación, creé un CursorLoaderdescendiente para administrar un cursor SQLite, además del constructor, solo necesitaba anular el loadInBackgroundmétodo para reemplazar la consulta del proveedor con mi consulta del cursor
Jose_GD
14

SimpleCursorLoader es una solución simple, sin embargo, no admite la actualización del cargador cuando cambian los datos. CommonsWare tiene una biblioteca loaderex que agrega un SQLiteCursorLoader y admite volver a consultar los cambios de datos.

https://github.com/commonsguy/cwac-loaderex

emmby
fuente
2
Sin embargo, para hacer uso de la nueva consulta automática, debe usar el mismo cargador para la interfaz de usuario y para las actualizaciones, lo que limita su usabilidad para los servicios en segundo plano.
ge0rg
12

Una tercera opción sería simplemente anular loadInBackground:

public class CustomCursorLoader extends CursorLoader {
    private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();

    @Override
    public Cursor loadInBackground() {
        Cursor cursor = ... // get your cursor from wherever you like

        if (cursor != null) {
            // Ensure the cursor window is filled
            cursor.getCount();
            cursor.registerContentObserver(mObserver);
        }

        return cursor;
    }
};

Esto también se encargará de volver a consultar su cursor cuando cambie la base de datos.

Solo advertencia: tendrá que definir a otro observador, ya que Google, en su sabiduría infinita, decidió hacer privado el paquete de ellos. Si pones la clase en el mismo paquete que el original (o el compatible), puedes usar el observador original. El observador es un objeto muy liviano y no se usa en ningún otro lugar, por lo que esto no hace mucha diferencia.

Timo Ohr
fuente
Mi observación en las pruebas rápidas es que registerContentObserver solo se llamará contra el cursor si el cursor está dirigido a un proveedor de contenido. ¿Puedes confirmar / negar esto?
Nick Campion
1
No necesariamente tiene que ser un ContentProvider. Pero el cursor debe estar registrado en una uri de notificación (setNotificationUri), y luego alguien (generalmente un ContentProvider, pero puede ser cualquier cosa) debe notificarlo llamando a ContentResolver.notifyChange.
Timo Ohr
4
Si. en su CustomLoader loadInBackground() , antes de devolver el cursor, diga que cursor.setNotificationUri(getContext().getContentResolver(), uri);el uri puede ser simplemente de una cadena aleatoria como Uri.parse("content://query_slot1"). Parece que no le importa que los uri realmente existan o no. Y una vez que hice la operación en DB. Say getContentResolver().notifyChange(uri, null);haría el truco. Entonces puedo crear algunas "consultas uri slot" en un archivo contante para la aplicación con un pequeño número de consultas. Probé la inserción del registro DB en tiempo de ejecución y parece que funciona, pero todavía dudo que sea una buena práctica. ¿Cualquier sugerencia?
Yeung
Estoy usando este método con la sugerencia de @Yeung y todo funciona, incluida la recarga automática del cursor en la actualización de la base de datos.
DavidH
no lo necesita ningún unregisterContentObserver?
GPack
2

La tercera opción propuesta por Timo Ohr, junto con los comentarios de Yeung, proporcionan la respuesta más simple (navaja de Occam). A continuación se muestra un ejemplo de una clase completa que me funciona. Hay dos reglas para usar esta clase.

  1. Extienda esta clase abstracta e implemente los métodos getCursor () y getContentUri ().
  2. Cada vez que cambie la base de datos subyacente (por ejemplo, después de una inserción o eliminación), asegúrese de llamar

    getContentResolver().notifyChange(myUri, null);

    donde myUri es el mismo devuelto por su implementación del método getContentUri ().

Aquí está el código de la clase que utilicé:

package com.example.project;

import android.content.Context;
import android.database.Cursor;
import android.content.CursorLoader;
import android.content.Loader;

public abstract class AbstractCustomCursorLoader extends CursorLoader
  {
    private final Loader.ForceLoadContentObserver mObserver = new Loader.ForceLoadContentObserver();

    public AbstractCustomCursorLoader(Context context)
      {
        super(context);
      }

    @Override
    public Cursor loadInBackground()
      {
        Cursor cursor = getCursor();

        if (cursor != null)
          {
            // Ensure the cursor window is filled
            cursor.getCount();
            cursor.registerContentObserver(mObserver);
          }

        cursor.setNotificationUri(getContext().getContentResolver(), getContentUri());
        return cursor;
      }

    protected abstract Cursor getCursor();
    protected abstract Uri getContentUri();
  }
John Moore
fuente