La mayoría de las aplicaciones tienen tareas en segundo plano, existiendo la necesidad de ejecutarlos en un hilo diferente al de la UI. Usando este tipo de tareas, esperamos que la aplicación responda a nuestras interacciones, y sea robusta, especialmente durante situaciones desfavorables. Pero, ¿podemos tener el mejor sistema de tareas en segundo plano?
Mejorando las tareas en segundo plano en Android
Android ofrece en su framework varias alternativas para crear este tipo de tareas, por ejemplo, AsyncTasks, Loaders y Services con un Thread Pool, pero tienen los siguientes problemas:
- AsyncTasks están muy acopladas a la UI. Se pueden resetar con la rotación de un dispositivo.
- Loaders son buenos para tareas de disco pero no para grandes peticiones de internet.
- Services, aunque están muy bien desacoplados de la UI, cuanto más servicios tengas ejecutando a la vez, más difícil será la concurrencia y priorización .
Para solventar estos problema, tenemos la librería Android Priority Job Queue, que se trata de una implementación de una Pila de Trabajos especificamente escrita para Android con la que podrás planificar tareas en segundo plano, mejorando la experiencia de usuario y la estabilidad de la aplicación.
Más ventajas
También, con esta librería puedes:
- Desacoplar la lógica de la aplicación de las actividades o fragmentos de una manera sencialla, haciendo que tu código sea más robusto, fácil de refactorizarlo y testearlo.
- Priorizar tareas en segundo plano, ejecutando unas antes que otras.
- Ejecutar tareas sólo si hay conexión a internet, y detectar los cambios de conexión para que la pila lance estos trabajos que se ejecuten cuando ésta vuelve.
- Mantener el estado de tu trabajo en el mismo punto en el que estaba después de una situación inesperada como un crash y restaurlo donde falló.
- Ejecutar trabajos en paralelo o agruparlos para ejecutarlos en serie.
- Planificar tareas, restrasándolas después de un determinado tiempo.
- Integrarlos con otros planificadores de trabajos como JobScheduler o GCMNetworkManager.
- etc.
¿Cómo empiezo a usar la librería?
Primero, necesitas incluir la librería en tu proyecto. Está disponible en el repositorio de Maven Central, así que simplemente, añade la siguiente línea en las dependencias de tu script gradle:
compile 'com.birbit:android-priority-jobqueue:2.0.0'
Recomendamos mucho usar una librería de eventos con Priority Job Queue, por ejemplo Event Bus de Green Robot.
compile 'org.greenrobot:eventbus:3.0.0'
Segundo, necesitas configurar tu JobManager. Hay una extensa documentación de cómo configurar en su página wiki. Por ejemplo,
Configuration.Builder builder = new Configuration.Builder(context)
.minConsumerCount(1) // always keep at least one consumer alive
.maxConsumerCount(3) // up to 3 consumers at a time
.loadFactor(3) // 3 jobs per consumer
.consumerKeepAlive(120) // wait 2 minute
.customLogger(new CustomLogger() {
private static final String TAG = "JOBS";
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public void d(String text, Object... args) {
Log.d(TAG, String.format(text, args));
}
@Override
public void e(Throwable t, String text, Object... args) {
Log.e(TAG, String.format(text, args), t);
}
@Override
public void e(String text, Object... args) {
Log.e(TAG, String.format(text, args));
}
@Override
public void v(String text, Object... args) {
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.scheduler(FrameworkJobSchedulerService.createSchedulerFor(context,
AppJobService.class), true);
} else {
int enableGcm = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
if (enableGcm == ConnectionResult.SUCCESS) {
builder.scheduler(GcmJobSchedulerService.createSchedulerFor(context,
AppGcmJobService.class), true);
}
}
mJobManager = new JobManager(builder.build());
Tercero, necesitas crear tus trabajos según tus preferencias. Mira en este enlace para saber con qué parámetros puedes configurar cada trabajo.
public FetchLocationByAddressJob(String address) {
super(new Params(JobConstants.PRIORITY_NORMAL)
.requireNetwork()
.singleInstanceBy(TAG)
.addTags(TAG)
);
...
}
Además del constructor del trabajo, necesitas sobreescribir estos métodos:
- onAdded: Llamado cuando el trabajo se ha añadido correctamente a la pila. Es un magnífico lugar para decirle a la UI a través de un evento que el trabajo se ejecutará cuando se reunan sus requisitos.
- onRun: Aquí es donde va toda la lógica de la tarea. Por ejemplo, una peticióna a un servicio web.
- shouldReRunOnThrowable: En este método podemos detectar si queremos volver a ejecutar el trabajo ante una situación inesperada o cancelarlo. Por ejemplo. imaginad que una petición necesita autenticación y el token ya no es válido, entonces puedes cancelar este trabajo y notificar a la app que el usuario debe de autenticarse.
- onCancel: Si se ejecuta dicha función, quiere decir que el trabajo ha fallado ya sea porque se ha excedido el número de reintentos que tiene el trabajo o porque se ha cancelado en el método shouldReRunOnThrowable.
Y por último, crea y añade trabajos a la pila, se ejecutarán cuando se reunan sus requisitos.
public class FetchLocationByAddressJob extends Job {
private String mAddress;
public static final String TAG = FetchLocationByAddressJob.class.getCanonicalName();
public FetchLocationByAddressJob(String address) {
super(new Params(JobConstants.PRIORITY_NORMAL)
.requireNetwork()
.singleInstanceBy(TAG)
.addTags(TAG)
);
mAddress = address;
}
@Override
public void onAdded() {
// Store address in database
FakeDatabase.setLastAddress(getApplicationContext(), mAddress);
}
@Override
public void onRun() throws Throwable {
String address = FakeDatabase.getLastAddress(getApplicationContext());
GeocodingInterface service = AppRetrofitManager.getGeocodingInterface();
Call request = service.getInfoByAddress(address);
Geocode geocode = AppRetrofitManager.performRequest(request);
// Ensure we have information about the search address
if (geocode == null || geocode.getResults().size() <= 0) {
EventBus.getDefault().post(new LocationFailedEvent());
return;
}
// Just retrieve the first result
EventBus.getDefault().post(new LocationFetchedEvent(geocode.getResults().get(0)));
}
@Override
protected void onCancel(int cancelReason, @Nullable Throwable throwable) {
EventBus.getDefault().post(new LocationFailedEvent());
}
@Override protected RetryConstraint shouldReRunOnThrowable(@NonNull Throwable throwable, int runCount, int maxRunCount) {
if (throwable instanceof ErrorRequestException) {
ErrorRequestException error = (ErrorRequestException) throwable;
int statusCode = error.getResponse().raw().code();
if (statusCode >= 400 && statusCode < 500) {
return RetryConstraint.CANCEL;
}
}
return RetryConstraint.RETRY;
}
}
Si quieres ver un proyecto entero usando esta este tipo de tareas en segundo plano con esta librería, echa un vistazo a esta app.
Consejo
Nunca uses llamadas síncronas de Volley dentro de un trabajo, usa Retrofit mejor.
Cuando llevas ya unos cuantos trabajos ejecutados, a veces, a cabo del tiempo, te puedes dar cuenta que todo este tipo de tareas en segundo plano que hacen una petición síncrona con Volley fallan por un extraño error. Da igual que canceles todos los trabajos y reinicies la pila cuando ésto pasa. Todos fallarán incluso poniendo un timeout mayor que 0, diferente al de por defecto.
Depurando puedes ver que el servidor devuelve una respuesta correcta, pero falla cuando Volley comprueba el estado de la petición.
Mis sospechas por lo que pasa este error es porque Volley cachea las peticiones, originando algún tipo de bloqueo que hace que todos los demás fallen.
Así que, no uses Volley con esta librería ya que te volverá loco.
Artículos relacionados
Informes en formato PDF en Android
ASO – Posiciona tu app en el App Store o Google Play
Muchas gracias por este post.
Andaba buscado precisamente, como priorizar algunas tareas de otras.
Lamentablemente android no permite hacer algo así con su interfaz, por lo que hay que hacer lo que se comenta en este artículo.
saludos!
Hola !, Buen post, pero podrías decir por que utilizar Retrofit ??, Que diferencias tiene entre Volley ?..
Hola,
Yo antes usaba Volley en todos mis proyectos, pero cuando descubrí Retrofit cambié de opinión. Para describir las diferencias entre una y otra librería, se debería de hacer en otra entrada en el blog, no es algo simple.
Yo si quieres te comento las principales diferencias: simplicidad, es una librería Java (no necesita de una instancia de Context para funcionar), puedes usar varios conversores (JSON, XML) y llamadas Multipart con la misma instancia de Retrofit…
Además, Volley tiene un cacheo en memoria de las requests, para mi gusto malo, que al cabo del tiempo empieza a dejar de llamar con llamadas síncronas.
Como ya he dicho anteriormente, si tuviera que empezar un proyecto de nuevo, me decantaría sin ninguna duda por Retrofit. Pero también es un punto de vista personal, a lo mejor la posibilidad de extensión de Volley es esencial para ti y Retrofit no te conviene.