domingo, 11 de mayo de 2014

Loaders de Android (II): Implementar nuestro propio Loader


Implementar Loaders

Como el origen de nuestro interés por implementar nuestro propio Loader es la negativa a usar la clase CursorLoader, veamos que nos aporta esta clase para saber que deberíamos incluir en la nuestra:
  • Carga asíncrona de datos. La mayoría lo conseguimos heredando de AsyncLoader pero aún así tendremos que añadir código para manejar los datos dentro de la clase durante las distintas etapas. Afortunadamente este código es totalmente reutilizable, así que nos puede servir para distintos Loaders.
  • Consulta encapsulada. Al usar el modelo de consulta de los ContentProvider, CursorLoader proporciona una manera de expresar las consultas usando diversos parámetros (uri, projection...). Como queremos cumplir con el primer apartado, tendremos que ser capaces de expresar las consultas sin llegar a realizarlas.
  • Monitorización de datos. Cuando los datos originales cambian CursorLoader es notificado. Podemos obtener esta funcionalidad fácilmente, aunque nos obligará a añadir cierta funcionalidad a la fuente de datos.

Estos serían los tres "pilares" de nuestro Loader. No solo podemos conseguir implementarlos todos sino que podemos tener una solución aún mas independiente de la fuente de datos y del formato de los datos y con una monitorización con mayor alcance.


Carga asíncrona

Para este apartado se sigue como fuente principal el artículo de Alex Lockwood sobre implementar Loaders. No solo recomiendo leer el artículo completo (son cuatro partes), sino que también recomiendo echarle un vistazo a su blog sobre Android porque hay artículos bastante interesantes.

Como se ha comentado antes, gran parte de esta funcionalidad se consigue heredando de la clase abstracta AsyncLoader. La implementación de nuestro Loader pasa por implementar correctamente algunas de las funciones definidas en AsyncLoader:
  • loadInBackground(): Es la función donde realizaremos la lectura de datos en sí. En este primer apartado no tendremos la consulta completa ya que profundizaremos en eso en el segundo apartado.
  • deliverResult(): Función llamada para enviar el resultado de la lectura al LoaderManager.
  • onStartLoading(): Función llamada cuando LoaderManager inicia nuestro Loader.
  • onStopLoading(): Función llamada cuando LoaderManager detiene nuestro Loader.
  • onReset(): Función llamada cuando LoaderManager desactiva nuestro Loader.
  • onCanceled(): Función llamada cuando LoaderManager cancela la lectura de datos.
  • releaseResources(): Esta realmente no es un método de AsyncLoader sino del código de Lockwood. Sirve para hacer una limpieza de los resultados de la lectura cuando estos pasan a ser inválidos. Por ejemplo, en el caso de usar Cursor (allá vosotros), tendríamos que incluir aquí una llamada a close().

En el artículo de Lockwood se nos presenta una plantilla para implementar nuestros Loaders. Usaremos este código para implementar nuestro primer pilar, pero lo iremos modificando para conseguir un loader más genérico. A continuación vemos una versión "traducida" del código:

public class SampleLoader extends AsyncTaskLoader<List<SampleItem>> {

    // Mantenemos una referencia al resultado obtenido
    private List<SampleItem> mData;

    public SampleLoader(Context ctx) {
        super(ctx);
    }

    @Override
    public List<SampleItem> loadInBackground() {
        List<SampleItem> data = new ArrayList<SampleItem>();
        // TODO: Realizar la verdadera consulta (apartado 2)
        return data;
    }

    @Override
    public void deliverResult(List<SampleItem> data) {
        if (isReset()) {
            // Si el Loader ha sido desactivado liberamos los resultados y salimos.
            releaseResources(data);
            return;
        }

        // Mantenemos una referencia a los resultados anteriores para que
        // el recolector de basura no los elimine hasta que hallamos entregado los
        // nuevos resultados.
        List<SampleItem> oldData = mData;
        mData = data;
    
        if (isStarted()) {
            // Si el Loader está en estado iniciado entregamos los resultados.
            // La clase AsyncLoader se encargará de ello.
            super.deliverResult(data);
        }

        // Liberamos los viejos resultados ahora que hemos entregado los nuevos.
        if (oldData != null && oldData != data) {
            releaseResources(oldData);
        }
    }

    @Override
    protected void onStartLoading() {
        if (mData != null) {
            // Si ya tenemos resultados los entregamos.
            deliverResult(mData);
        }

        //TODO: Registrarse a la fuente de datos para monitorizar cambios (apartado 3)

        if (takeContentChanged() || mData == null) {
            // Si hemos sido notificados de cambios o si no tenemos resultados
            // iniciamos la lectura de datos.
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        // Intentamos cancelar la lectura de datos.
        cancelLoad();
    }

    @Override
    protected void onReset() {
        // Nos aseguramos de que el Loader ha sido detenido
        onStopLoading();

        // Liberamos los resultados que manteniamos
        if (mData != null) {
            releaseResources(mData);
            mData = null;
        }

        // TODO: Eliminar el registro de la fuente de datos (apartado 3)
    }

    @Override
    public void onCanceled(List<SampleItem> data) {
        // intentamos cancelar la lectura de datos.
        super.onCanceled(data);

        // Liberamos los resultados
        releaseResources(data);
    }

    protected void releaseResources(List<SampleItem> data) {
        // En este ejemplo no es necesario añadir código aqui.
    }
}


Con esto ya tenemos un Loader que realizará la lectura de datos en segundo plano. Sin embargo, el hecho de que la lectura en sí forme parte de su implementación hace necesario que tengamos una clase Loader por cada lectura distinta. Este problema se soluciona en el siguiente apartado.


Consulta encapsulada

Lo bueno del código de Lockwood es que, aparte del método loadInBackground(), todo es reutilizable. Lo único que varía es la consulta de datos en sí. Por ello debemos extraer la consulta y así podremos reutilizar todo lo demás. Además podemos conseguir un añadido sobre CursorLoader: tener un Loader independiente del tipo de dato que devuelva la consulta. Hay dos maneras de conseguir esto: mediante herencia y mediante composición.

Mediante herencia: Convertimos la implementación antes vista en una clase abstract AbsLoader<D> (ahora debemos parametrizarla para manejar cualquier tipo de dato). Mantenemos la implementación de todos los métodos excepto de loadInBackground(), que dejaremos sin implementar. Para tener lecturas concretas solo tenemos que crear una clase que herede de AbsLoader y que proporcione la lectura de datos implementando loadInBackground().

public abstract class AbsLoader<D> extends AsyncTaskLoader<D> {
    // ...
}


Mediante composición: Delegaremos la lectura de datos en un objeto externo. Para ello creamos la interfaz Query<D> con un solo método, execute(), que nos devolverá un objeto de tipo D. Solo queda que modifiquemos SampleLoader para que utilice este objeto en loadInBackground():

public class BaseLoader<D> extends AsyncTaskLoader<D> {

    // ...
    private Query<D> query;

    public SampleLoader(Context ctx, Query<D> query) {
        this.query = query;
        super(ctx);
    }

    @Override
    public D loadInBackground() {
        return query.execute();
    }

    // ...
}


Sea cual sea el método escogido ya tenemos un Loader básico que podremos reutilizar con distintos tipos y fuentes de datos.


Monitorización de datos

Si queremos podemos hacer que nuestro Loader sea notificado cuando los datos que ha leído cambian en la fuente original. Lo más sencillo es aplicar un patrón observador. No es la intención de este post el explicar este patrón de diseño. Para ello hay mejores fuentes (como esta o esta otra).

Definimos una interfaz Observer (con un solo método update()) que nuestro Loader implementará de la siguiente manera:

@Override
public void update(){
    onContentChanged();
}

Definimos la interfaz Subject que será la encargada de administrar los Observer y notificarles. Podemos hacer que la propia fuente de datos implemente la interfaz o creamos explícitamente una clase que la implemente y en la fuente de datos guardamos una instancia de esta clase. En cualquier caso debemos añadir una llamada a notifyObservers() al final de cada método que realice cambios en los datos.

Por último solo queda modificar nuestra clase Loader para que acepte objetos Subject y se registre o se elimine del registro cuando sea necesario (en el lugar donde se colocaron comentarios TODO en el primer apartado):

public class BaseLoader<D> extends AsyncTaskLoader<D> implements Observer {
    private D mData;
    private Query<D> mQuery;
    private ArrayList<Subject> mSubjects;
    private boolean registered;

    public ObserverLoader(Context context, Query<D> query){
        super(context);
        registered = false;
        mQuery = query;
        mSubjects = new ArrayList<Subject>();
    }

    public ObserverLoader(Context context, Query<D> query, Subject dataSubject) {
        super(context);
        registered = false;
        mQuery = query;
        mSubjects = new ArrayList<Subject>();
        if(dataSubject != null)
            mSubjects.add(dataSubject);
    }

    public ObserverLoader(Context context, Query<D> query, List<Subject> dataSubjects){
        super(context);
        registered = false;
        mQuery = query;
        mSubjects = new ArrayList<Subject>(dataSubjects);
    }

    // ...
    @Override
    protected void onStartLoading(){
        // ...
        if(!registered){
            registered = true;
            for(Subject sub : mSubjects)
                sub.registerObserver(this);
        }
        // ...
    }

    // ...

    @Override
    protected void onReset() {
        // ...

        if(registered){
            registered = false;
            for(Subject sub : mSubjects)
                sub.unregisterObserver(this);
        }
    }

    // ...

    @Override
    public void update(){
        onContentChanged();
    }
    // ...
}

Podemos ver que nuestro Loader puede registrarse a más de un Subject. Esto nos da la posibilidad de que el Loader sea notificado no solo sobre cambios en los datos originales, sino cuando datos que afectan a la consulta (como filtros) cambian.

Con esto ya tenemos Loader completos y reutilizables. Si queréis ver o usar una implementación ya realizada de estos Loader, tenéis la ObserverLoaderLibrary. Cualquier feedback sobre este post o sobre la biblioteca (bugs, recomendaciones, aportes...) es agradecido.

No hay comentarios:

Publicar un comentario