domingo, 11 de mayo de 2014

Loaders de Android (I): Introducción y uso


¿Qué es un Loader?

Como introducción podemos decir que un objeto Loader proporciona funcionalidad para realizar una carga asíncrona de datos. Fueron introducidos en la versión 3.0 de Android y hasta entonces si querías realizar alguna lectura de datos en segundo plano tenías que proporcionar tu el AsyncTask que se encargará de ejecutarla, los mecanismos de sincronización... etc

Si además querías manejar correctamente el ciclo de vida de los datos leídos lo único que proporcionaba Android era la función Activity#startManagingCursor(Cursor). No solo quedaba restringido al uso de Cursor para manejar datos sino que realizaba todas las "recargas" de datos en la hebra principal.

En Android realizar una operación costosa como leer de una base de datos o realizar una comunicación web en la hebra principal es uno de los principales sinónimos de mal desarrollo. La hebra principal es la hebra de la IU, por lo que bloquear esta hebra implica bloquear la aplicación de cara al usuario, no pudiendo recibir eventos de entrada ni mostrar animaciones o datos como salida. Esto empobrecería enormemente la experiencia de usuario, que incluso podría llegar a tener la sensación de que la app ha dejado de funcionar. Además Android, a partir de la versión 4.0, responde a llamadas bloqueantes sobre la hebra principal con un error que cierra automáticamente la aplicación.


Cómo usar un Loader

Los componentes principales a tener en cuenta a la hora de usar Loaders en nuestra aplicación son la clase LoaderManager y la interfaz LoaderCallbacks.

Toda Activity o Fragment tiene asociada una instancia única de LoaderManager a la que podemos acceder usando la función getLoaderManager(). Usando esta instancia podemos iniciar, reiniciar y destruir Loaders sin preocuparnos por bloquear la hebra principal. Tampoco tenemos que preocuparnos por manejar el ciclo de vida de los Loader ya que estos quedan "ligados" al ciclo de vida de la Activity/Fragment propietaria del LoaderManager. Es decir, si el propietario es iniciado, el Loader es iniciado, si se para (al dejar de ser visible), el Loader se para... etc

Por su parte la interfaz LoaderCallbacks define los métodos a implementar para instanciar los Loader y recoger los resultados de su ejecución:
  • Loader<D> onCreateLoader(int id, Bundle args): Instancia un nuevo Loader.
    • id: Identificador único del Loader.
    • args: Parámetros adicionales necesarios en la creación del Loader.
  • onLoadFinished(Loader<D> loader, D data): Método llamado cuando un Loader ha terminado de leer datos.
    • loader: El Loader que ha finalizado.
    • data: Los datos recibidos de la lectura del Loader.
  • onLoaderReset(Loader<D> loader): Método llamado cuando un Loader ha sido reiniciado y por lo tanto sus datos pasan a ser inválidos.
    • loader: Loader reiniciado.

En la práctica acabaremos usando como máximo tres funciones de LoaderManager para manejar toda nuestra lectura de datos:
  • Loader<D> initLoader(int id, Bundle args, LoaderCallBacks<D> callbacks): Inicia un nuevo Loader y comienza a leer datos. Si ya existe un Loader con ese identificador se reutiliza sin volver a ejecutar la lectura de datos.
  • Loader<D> restartLoader(int id, Bundle args, LoaderCallBacks<D> callbacks): Reinicia un Loader y comienza a leer datos. Si no existe un Loader con ese identificador creará uno nuevo.
  • destroyLoader(int id): Elimina un Loader. Si el Loader ya ha sido iniciado y esta leyendo datos, la lectura se cancela.

Como lo mejor es verlo en un ejemplo, veamos como usaríamos los Loader en el caso de tener una lista que quisiésemos llenar con los resultados de una consulta, que es un caso práctico que seguro nos encontraremos en muchas aplicaciones.

Nota: He obviado partes del código para concentrarnos en la parte del manejo del Loader.

public class MyListFragment extends ListFragment implements LoaderCallbacks<Cursor>{
    @Override
    public void onCreate(Bundle savedInstanceState){
        //...
        // Iniciamos la carga de datos
        // Como solo tenemos un loader el valor del identificador no importa
        getLoaderManager().initLoader(0, null, this);
        //...
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        // Instanciamos el Loader
        // Este apartado se explica mejor a continuacion
        return new CursorLoader(/*...*/);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        // Hacemos uso de los datos recibidos
        // En este caso cambiamos el Cursor del ListAdapter de la lista
        // que se encargará de mostrar los datos al usuario
        ((CursorAdapter) getListAdapter()).swapCursor(cursor);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        // Eliminamos el Cursor que contenia el ListAdapter
        // ya que los datos ya no son validos
        ((CursorAdapter) getListAdapter()).swapCursor(null);
    }
}


Y con eso ya tendríamos una lista funcionando. Solo una línea en la función onCreate() e implementar tres métodos de una interfaz.

Como se puede ver, en el método onCreateLoader() hemos creado una instancia de CursorLoader. He obviado los parámetros del constructor porque no son relevantes para el uso de Loaders. Si queréis aprender a usar CursorLoader antes os recomiendo que entendáis como funcionan las consultas con ContentProviders de Android.

Si habéis consultado la documentación sobre Loader habréis visto que tanto Loader<D> como AsyncLoader<D> son clases abstractas. La única implementación de Loader que proporciona Android es la clase CursorLoader. Es fácil de usar y nos proporciona carga en segundo plano y monitorización automática de cambios en los datos. Sin embargo tiene una importante restricción: solo funciona con consultas a ContentProviders. En el caso de que queramos usar los Loader sin usar ContentProvider (o sin usar Cursor tampoco) la única solución es implementar nosotros nuestro propio Loader.

No hay comentarios:

Publicar un comentario