sábado, 20 de septiembre de 2014

Cargadores

Los cargadores hicieron su primera aparición con la versión 3.0 de Android. El objetivo de un cargador es el de facilitar la carga y actualización de los datos usados en una actividad o fragmento. Entre sus características, podemos destacar:
  • Pueden ser usados desde las actividades y fragmentos de nuestra aplicación.
  • La carga de los datos puede realizarse de manera asíncrona.
  • Detectan cambios en la fuente de datos y entregan los nuevos resultados.
  • Si la configuración del dispositivo cambia, el cargador recuperará los datos de la última consulta sin tener que volver a repetirla, con el consiguiente ahorro de tráfico y procesamiento de datos.

Un resumen de la API.

  • LoaderManager.  Clase abstracta a la que podremos acceder desde una actividad o fragmento y que se encargará de gestionar las instancias de los cargadores existentes derivados de la clase Loader.
  • LoaderManager.LoaderCallbacks. Interfaz utilizada por el gestor de cargadores LoaderManager. Por ejemplo, para crear un nuevo cargador utilizaremos el método onCreateLoader().
  • Loader. Clase abstracta responsable de la carga asíncrona de datos. Se trata de la clase base de un cargador. Normalmente usaremos la clase derivada CursorLoader, aunque podríamos, si fuera necesario, hacer nuestra propia implementación. Mientras un cargador está activo, éste controlará la fuente de datos ante posibles cambios y si éstos se producen, se encargará de entregarnos los nuevos resultados.
  • AsyncTaskLoader. Cargador abstracto que cuenta con una tarea asíncrona AsynkTask para hacer el trabajo.
  • CursorLoader. Una subclase de AsyncTaskLoader que hace consultas a través de ContentResolver y nos devolverá un Cursor. Esta clase implementa la clase abstracta Loader y usa un AsyncTaskLoader para la carga asíncrona de datos desde un ContentProvider. Al utilizar su propia hebra de ejecución, la carga de datos no afectará a la hebra de la IU.

Estos son los elementos básicos a tener en cuenta a la hora de usar cargadores en nuestras aplicaciones. De ellos, al menos, tendremos que hacer uso del LoaderManager para iniciar nuestro cargador y de una implementación de la clase Loader como puede ser la clase CursorLoader.

Usando los cargadores en una aplicación.

Para usar un cargador, necesitaremos los siguientes requisitos:
  • Una actividad o fragmento.
  • La instancia del gestor de cargadores LoaderManager.
  • Un CursorLoader para la carga de contenidos asociados a un ContentProvider. También podemos implementar nuestro propio Loader o AsyncTaskLoader para cargar los datos desde una fuente de naturaleza distinta.
  • Una implementación de la interfaz LoaderManager.LoaderCallbacks. Es desde donde vamos a crear nuevos cargadores y gestionar las referencias a los ya existentes.
  • Una manera de visualizar los datos cargados, como el adaptador SimpleCursorAdapter.
  • Una fuente de datos de tipo ContentProvider complementaria a nuestro cargador de tipo CursorLoader.

Iniciando el cargador.

El gestor de cargadores LoaderManager puede gestionar varias instancias de cargadores Loader desde una actividad o fragmento. Solo hay una instancia del gestor de cargadores (patrón Sigleton).

Normalmente, iniciaremos nuestros cargadores desde el método onCreate() de una actividad o desde el método onActivityCreated() de un fragmento:
// Tanto si existe como si no, siempre tendremos que iniciar el cargador.
getLoaderManager().initLoader(0, null, this);
El método initLoader() cuenta con los siguientes parámetros:
  • Un ID único que identifica al cargador. En el ejemplo, el ID vale 0.
  • Argumentos opcionales que utilizaremos construir el nuestro cargador. En el ejemplo, los argumentos valen null.
  • Una implementación de la interfaz LoaderManager.LoaderCallbacks que el LoaderManager utilizará para gestionar la carga de datos. En el ejemplo, la interfaz es implementada por el objeto this (nuestra actividad o fragmento).
El método initLoader() se asegura de que el cargador está iniciado y activo. Cuando invocamos este método, pueden suceder dos cosas:
  • Si el ID del cargador especificado ya existe, éste será reutilizado.
  • Si el ID del cargador especificado no existe, initLoader() ejecuta el método onCreateLoader() de su interfaz LoaderManager.LoaderCallbacks. En este método implementaremos lo necesario para instanciar y devolver el nuevo cargador.
En cualquier caso, la implementación de la interfaz LoaderManager.LoaderCallbacks asociada al cargador se utilizará cada vez que el cargador cambie su estado. Ten en cuenta, que si a la hora de llamar al método initLoader() el cargador ya disponía de datos de alguna consulta anterior, se llamará directamente al método onLoadFinished().

Aunque el método initLoader() devuelve el cargador recién creado, no tienes por qué almacenar su instancia en una variable. El LoaderManager se encargará de gestionar el cargador por nosotros: se encargará de iniciar o parar la carga de datos cuando sea necesario y de mantener su estado y contenido. Es por ello por lo que no tendremos que usar los cargadores directamente y en su lugar implementaremos su interfaz LoaderManager.LoaderCallbacks.

Reiniciando el cargador.

A veces, nos interesará descartar los datos antiguos y forzar una nueva carga. Para volver a cargar los datos usaremos el método restartLoader(). En el siguiente ejemplo, cargamos los datos cada vez que el usuario cambia el criterio de búsqueda SearchView.OnQueryTextListener:
public boolean onQueryTextChanged(String newText) {
    // Llamado cada vez que el texto de búsqueda de la barra de acción cambia.
    // Se actualiza el filtro y se actualizan los datos del cargador teniendo
    // en cuenta el nuevo filtro.
    mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
    getLoaderManager().restartLoader(0, null, this);
    return true;
}

La interfaz LoaderManager.LoaderCallbacks.

Los cargadores, en particular CursorLoader, guardan una copia de los datos una vez ha finalizado su carga. Es decir, que podremos conservar los datos a lo largo del ciclo de vida de nuestras actividades y fragmentos.

Implementar la interfaz LoaderManager.LoaderCallbacks nos permitirá interactuar con el LoaderManager.
La interfaz incluye los siguientes métodos:
  • onCreateLoader(). Crea un nuevo cargador Loader y le asocia el ID especificado.
  • onLoadFinished(). Se invoca cada vez que la carga de datos finaliza.
  • onLoaderReset(). Se invoca cada vez que se reinicia el cargador, haciendo que sus datos ya no estén disponibles.

onCreateLoader()

Cuando accedemos al cargador a través del método LoaderManager.initLoader(), el gestor de cargadores comprueba la existencia de su argumento ID. Si el ID especificado no existe, llama al método LoaderCallbacks.onCreateLoader() que será el encargado de crear la nueva instancia para nuestro cargador.

En el siguiente ejemplo, el método onCreateLoader(), crea un CursorLoader. Para crear un CursorLoader, tendremos que especificar la información necesaria para realizar consultas sobre un ContentProvider. Para ello, necesitaremos:
  • Uri. Enlace al contenido.
  • projection. Cláusula de proyección. Si especificamos valor null, se devolverán todas las columnas.
  • selection. Cláusula de selección.
  • selectionArgs. Argumentos de la cláusula de selección. Evita ataques por inyección SQL.
  • sortOrder. Cláusula de ordenación. Si especificamos valor null, se utilizará el orden natural de almacenamiento.
...
import android.provider.ContactsContract.Contacts;
...

// Cadena de texto introducida por el usuario
String mCurFilter;

// Definimos la cláusula de proyección.
static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
    Contacts._ID,
    Contacts.DISPLAY_NAME,
    Contacts.CONTACT_STATUS,
    Contacts.CONTACT_PRESENCE,
    Contacts.PHOTO_ID,
    Contacts.LOOKUP_KEY,
};
...

// Es llamado si el cargador no existe.
public Loader onCreateLoader(int id, Bundle args) {
        
    // Construimos la URI base a partir del filtro actual mCurFilter.
    Uri baseUri;

    if (mCurFilter != null) {
        baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                  Uri.encode(mCurFilter));
    } else {
        baseUri = Contacts.CONTENT_URI;
    }

    // Definimos la cláusula de selección.
    String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
            + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
            + Contacts.DISPLAY_NAME + " != '' ))";
    
    // Creamos el cargador de tipo CursorLoader y lo devolvemos.
    return new CursorLoader(getActivity(), baseUri,
            CONTACTS_SUMMARY_PROJECTION, select, null,
            Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}

onLoadFinished()

Este método será invocado cada vez que el cargador finalice la carga de datos. El método se invoca antes de que los datos de la última carga sean eliminados. En el ejemplo, utilizamos un adaptador (SimpleCursorAdapter) para mostrar los datos cargados en el cursor (CursorLoader):
// Definimos el adaptador para mostrar los datos.
SimpleCursorAdapter mAdapter;
...

public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    // Cambiamos los datos del adaptador por los nuevos (visualizamos el resultado).
    // El cursor con la carga anterior será cerrado automáticamente y
    // no tendremos que preocuparnos por ello.
    mAdapter.swapCursor(data);
}

onLoadReset()

Método que será invocado cuando reiniciamos el cargador haciendo que sus datos ya no estén disponibles. Esto nos permitirá controlar cuándo son eliminados los datos y actuar en consecuencia:
// Definimos el adaptador para mostrar los datos.
SimpleCursorAdapter mAdapter;
...

public void onLoaderReset(Loader loader) {
    // Se eliminan los datos del cargador y mostramos la lista vacía.
    mAdapter.swapCursor(null);
}

No hay comentarios:

Publicar un comentario