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