Ponte a trabajar con Task
Sabemos que algunas de las funciones de Firebase para Android harán el trabajo por ti y notificarán una Task una vez completada. ¿Y si quisieras crear tus propias Tasks para realizar trabajo en subprocesos? La API Task te ofrece las herramientas para que puedas hacerlo. Si deseas trabajar con la API Task sin tener que integrar Firebase en tu app, puedes obtener la biblioteca con una dependencia en build.gradle:
compile 'com.google.android.gms:play-services-tasks:9.6.1'
Pero si
integras Firebase, se incluirá esta biblioteca de forma gratuita, por lo que no será necesario solicitarla específicamente en ese caso.
Solo hay un método (con dos variantes) que puedes utilizar para iniciar una nueva tarea. Puedes usar el método estático, llamado "call" en la clase de utilidad Tasks para esto. Las variantes son las siguientes:
Task<TResult> call(Callable<TResult> callable)
Task<TResult> call(Executor executor, Callable<TResult> callable)
Al igual que con addOnSuccessListener(), tienes una versión de
call() que ejecuta el trabajo en el subproceso principal y otra que envía el trabajo a un Ejecutor. Debes especificar el trabajo que se debe realizar dentro del
Callable pasado. Un objeto Callable de Java es similar a un Runnable, salvo que está parametrizado por algún tipo de resultado, y ese tipo se convierte en el tipo de objeto devuelto del método call(). Este tipo de resultado se convierte luego en el tipo de la Task devuelta por call(). A continuación, se presenta un Callable realmente sencillo que solo devuelve un String:
public class CarlyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Call me maybe";
}
}
Observa que CarlyCallable está parametrizado por String, lo que significa que su método call() debe devolver un String. Ahora puedes crear una Task a partir de él con una sola línea:
Task<String> task = Tasks.call(new CarlyCallable());
Después de que se ejecuta esta línea, puedes estar seguro de que el método call() en CarlyCallable se invocará en el subproceso principal y puedes agregar un receptor a la Task para encontrar el resultado (a pesar de que ese resultado es completamente predecible). Los Callables más interesantes pueden incluso cargar algunos datos de una base de datos o de un extremo de red, por lo que querrás que esos Callables de bloqueo se ejecuten en un Ejecutor que use la segunda forma de
call() que acepta al Ejecutor como el primer argumento.
Working on the Chain Gang o «Atados por la cadena»
Digamos, a modo de ejemplo, que deseas procesar el resultado del String de la Task CarlyCallable después de que se ha generado. Imagina que no nos interesa tanto el texto del String resultante en sí, sino una lista de las palabras individuales del String. Pero no necesariamente deseamos modificar CarlyCallable porque realiza exactamente la acción que debe realizar, y podría usarse en otros lugares tal como está escribo ahora. En lugar de ello, preferiríamos encapsular la lógica que divide las palabras en su propia clase, y usarla después de que CarlyCallable devuelva el String. Podemos hacerlo con una interfaz
Continuation. La implementación de la interfaz Continuation toma el resultado de una Task, realiza un procesamiento y devuelve un objeto de resultado, no necesariamente del mismo tipo. Esta es una Continuation que divide un string de palabras en una Lista de strings con cada palabra:
public class SeparateWays implements Continuation<String, List<String>> {
@Override
public List<String> then(Task<String> task) throws Exception {
return Arrays.asList(task.getResult().split(" +"));
}
}
Observa que la interfaz Continuation que se implementa aquí está parametrizada por dos tipos, un tipo de entrada (String) y uno de salida (Lista
). Los tipos de entrada y salida se usan en la firma del método solitario then() para definir lo que se supone que debe realizar. Cabe destacar el parámetro pasado a then(). Es una Task, y el String debe coincidir con el tipo de entrada de la interfaz Continuation. Este es el modo en que Continuation obtiene su entrada: extrae el resultado terminado de la Task completada.
Esta es otra Continuation que aleatoriza una Lista de strings:
public class AllShookUp implements Continuation<List<String>, List<String>> {
@Override
public List<String> then(@NonNull Task<List<String>> task) throws Exception {
// Randomize a copy of the List, not the input List itself, since it could be immutable
final ArrayList<String> shookUp = new ArrayList<>(task.getResult());
Collections.shuffle(shookUp);
return shookUp;
}
}
Y otra que une una Lista de strings en un solo string separado por espacios.
private static class ComeTogether implements Continuation<List<String>, String> {
@Override
public String then(@NonNull Task<List<String>> task) throws Exception {
StringBuilder sb = new StringBuilder();
for (String word : task.getResult()) {
if (sb.length() > 0) {
sb.append(' ');
}
sb.append(word);
}
return sb.toString();
}
}
Seguramente puedas ver a dónde apunto con esto. Unamos todo en una cadena de operaciones que aleatorice el orden de las palabras de un String de una Task inicial y genere un nuevo String con ese resultado:
Task<String> playlist = Tasks.call(new CarlyCallable())
.continueWith(new SeparateWays())
.continueWith(new AllShookUp())
.continueWith(new ComeTogether());
playlist.addOnSuccessListener(new OnSuccessListener<String>() {
@Override
public void onSuccess(String message) {
// The final String with all the words randomized is here
}
});
El método
continueWith() en la Task devuelve una nueva Task que representa el cálculo de la Task anterior después de haber sido procesada por la Continuation determinada. Entonces, lo que hacemos aquí es generar una cadena de llamadas a continueWith() para formar un proceso de operaciones que culmina en una Task final que espera que cada etapa termine antes de completarse.
Esta cadena de operaciones puede ser problemática si tiene que manejar Strings grandes; para evitarlo, vamos a modificarla para que todo el procesamiento se realice en otros subprocesos a fin de no bloquear el subproceso principal:
Executor executor = ... // you decide!
Task<String> playlist = Tasks.call(executor, new CarlyCallable())
.continueWith(executor, new SeparateWays())
.continueWith(executor, new AllShookUp())
.continueWith(executor, new ComeTogether());
playlist.addOnSuccessListener(executor, new OnSuccessListener() {
@Override
public void onSuccess(String message) {
// Do something with the output of this playlist!
}
});
Ahora, el Callable, todas las Continuations y el receptor de la Task final
se ejecutarán en algún subproceso determinado por el Ejecutor, liberando el subproceso principal para que se ocupe de las cuestiones relacionadas con la IU cuando aparezcan. Debe estar completamente libre de bloqueos.
La primera impresión puede ser que parezca un poco tonto separar todas estas operaciones en todas las clases diferentes. También podrías fácilmente escribir esto como unas pocas líneas en un solo método que realiza lo que se requiere. No debemos olvidar que este es un ejemplo simplificado que tiene por objeto destacar cómo pueden funcionar las Tasks. El beneficio de encadenar las Tasks y Continuations (incluso para funciones relativamente sencillas) se torna más evidente cuando tenemos en cuenta lo siguiente:
- ¿Cómo podrías incorporar nuevos orígenes de Strings de entrada? ¿Y si, además, tuviéramos un BlondieCallable? ¿Y un PaulSimonCallable?
- ¿Qué hay de las diferentes clases de procesos para los Strings de entrada, como una Continuation YouSpinMeRound que rotara el orden de los Strings en una lista una posición hacia la derecha (como un disco)?
- ¿Y si desearas que diferentes componentes de la segmentación de procesos se ejecutaran en diferentes subprocesos?
En la práctica, es más probable que utilices las continuaciones de Task para realizar una serie de cadena modular de filtro, mapa, y reducir las funciones en un conjunto de datos y mantener esas unidades de trabajo fuera del subproceso principal, si las colecciones pueden ser grandes. Me divertí con el tema de la música aquí.
¿Si la playlist se interrumpe?
Algo más que debes saber sobre las Continuations. Si se lanza una excepción de tiempo de ejecución durante el procesamiento en cualquier etapa del proceso, esa excepción normalmente se propagará hasta los receptores de falla en la Task final de la cadena. Puedes comprobar esto tú mismo en cualquier Continuation consultando a la Task de entrada si se completó correctamente con el método isSuccessful(). Puedes también llamar ciegamente a getResult() (como en los ejemplos anteriores) y, si existió una falla previa, se volverá a lanzar y finalizará automáticamente en la Continuation siguiente. Los receptores de la Task final de la cadena siempre deben comprobar si existe una falla (en caso de que exista la opción de una falla).
Entonces si, por ejemplo, CarlyCallable en la cadena anterior devolvió un valor nulo, esto hará que la Continuation SeparateWays lance una NullPointerException, la cual se propagará hasta el final de la Task. Y si tuviéramos una OnFailureListener registrada, se invocaría con la misma instancia de excepción.
Cuestionario
¿Cuál es el modo
más eficiente , con la cadena anterior, de conocer la cantidad de palabras en el string original, sin modificar ninguno de los componentes de procesamiento? Tómate un momento para pensarlo antes de seguir leyendo.
Probablemente, la respuesta sea más sencilla de lo que imaginas. La solución más obvia es contar la cantidad de palabras en el string de salida final, dado que solo se aleatorizó el orden. Pero existe un truco más. Cada llamada a continueWith() devuelve una nueva instancia de Task, pero esto no es visible aquí porque utilizamos una sintaxis de cadena para reunirlas en la Task final. Puedes interceptar cualquiera de esas Tasks y agregarles otro receptor, además de la Continuation siguiente:
Task<List<String>> split_task = Tasks.call(new CarlyCallable())
.continueWith(executor, new SeparateWays());
split_task =
.continueWith(executor, new AllShookUp())
.continueWith(executor, new ComeTogether());
split_task.addOnCompleteListener(executor, new OnCompleteListener<List<String>>() {
@Override
public void onComplete(@NonNull Task<List<String>> task) {
// Find the number of words just by checking the size of the List
int size = task.getResult().size();
}
});
playlist.addOnCompleteListener( /* as before... */ );
Cuando una Task finaliza, activará
ambas Continuations y también
todos los receptores agregados. Todo lo que hicimos aquí es interceptar la Task que captura la salida de la Continuation SeparateWays y escuchar la salida de esta directamente, sin afectar la cadena de Continuations. Con esta Task interceptada, solo tenemos que llamar a size() en la lista para obtener el conteo.
Para concluir (la parte 3 de esta serie)
Dejando de lado las bromas, la API Task hace que sea relativamente fácil expresar y ejecutar una segmentación secuencial de procesos de manera modular, y te ofrece la capacidad de especificar qué Ejecutor se utiliza en cada etapa del proceso. Puedes hacer esto integrando Firebase en tu app o no, usando tus propias Tasks o las que vienen en las API de Firebase. En la siguiente y última parte de esta serie, veremos cómo las Tasks pueden usarse en paralelo para iniciar varias unidades de trabajo al mismo tiempo.
Como siempre, si tienes alguna pregunta, puedes usar Twitter con el hashtag #AskFirebase o el
Grupo de Google firebase-talk. También tenemos un canal dedicado a Firebase Slack. También puede seguirme
@CodingDoug en Twitter para recibir notificaciones de la siguiente publicación de esta serie.
Por último, si te preguntas por todas las
canciones que mencioné en esta publicación, puedes encontrarlas aquí: