Las APIs de Task

La API de Task es la forma estándar de controlar operaciones asíncronas en los Servicios de Google Play. Proporciona una forma potente y flexible de administrar llamadas asíncronas, lo que reemplaza el patrón PendingResult anterior. Con Task, puedes encadenar varias llamadas, controlar flujos complejos y escribir controladores de éxito y error claros.

Controla los resultados de las tareas

Muchas APIs de los Servicios de Google Play y Firebase muestran un objeto Task para representar operaciones asíncronas. Por ejemplo, FirebaseAuth.signInAnonymously() muestra un Task<AuthResult> que representa el resultado de la operación de acceso. El Task<AuthResult> indica que, cuando la tarea se complete correctamente, mostrará un objeto AuthResult.

Para controlar el resultado de una Task, adjunta objetos de escucha que respondan a la finalización correcta, la falla o ambas:

Task<AuthResult> task = FirebaseAuth.getInstance().signInAnonymously();

Para controlar la finalización correcta de una tarea, adjunta un OnSuccessListener:

task.addOnSuccessListener(new OnSuccessListener<AuthResult>() {
    @Override
    public void onSuccess(AuthResult authResult) {
        // Task completed successfully
        // ...
    }
});

Para controlar una tarea que falló, adjunta un OnFailureListener:

task.addOnFailureListener(new OnFailureListener() {
    @Override
    public void onFailure(@NonNull Exception e) {
        // Task failed with an exception
        // ...
    }
});

Para controlar el éxito y la falla en el mismo objeto de escucha, adjunta un OnCompleteListener:

task.addOnCompleteListener(new OnCompleteListener<AuthResult>() {
    @Override
    public void onComplete(@NonNull Task<AuthResult> task) {
        if (task.isSuccessful()) {
            // Task completed successfully
            AuthResult result = task.getResult();
        } else {
            // Task failed with an exception
            Exception exception = task.getException();
        }
    }
});

Administra los subprocesos

De forma predeterminada, los objetos de escucha conectados a un Task se ejecutan en el subproceso principal (IU) de la aplicación. Esto significa que debes evitar realizar operaciones de larga duración en los objetos de escucha. Si necesitas realizar una operación de larga duración, puedes especificar un Executor que se usa para programar objetos de escucha en un subproceso en segundo plano.

// Create a new ThreadPoolExecutor with 2 threads for each processor on the
// device and a 60 second keep-alive time.
int numCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(numCores * 2, numCores *2,
        60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

task.addOnCompleteListener(executor, new OnCompleteListener<AuthResult>() {
    @Override
    public void onComplete(@NonNull Task<AuthResult> task) {
        // ...
    }
});

Usa objetos de escucha centrados en la actividad

Cuando necesites controlar los resultados de las tareas dentro de un Activity, es importante que administres el ciclo de vida de los objetos de escucha para evitar que se los llame cuando el Activity ya no sea visible. Para ello, puedes usar objetos de escucha centrados en la actividad. Estos objetos de escucha se quitan automáticamente cuando se llama al método onStop de tu Activity para que no se ejecuten después de que se detenga Activity.

Activity activity = MainActivity.this;
task.addOnCompleteListener(activity, new OnCompleteListener<AuthResult>() {
    @Override
    public void onComplete(@NonNull Task<AuthResult> task) {
        // ...
    }
});

Cómo encadenar tareas

Si usas un conjunto de APIs que muestran objetos Task en una función compleja, puedes encadenarlos con continuaciones. Esto te ayuda a evitar las devoluciones de llamada anidadas en profundidad y consolida la administración de errores para varias tareas encadenadas.

Por ejemplo, considera una situación en la que tienes un método doSomething que muestra un Task<String>, pero requiere un AuthResult como parámetro. Puedes obtener este AuthResult de forma asíncrona desde otro Task:

public Task<String> doSomething(AuthResult authResult) {
    // ...
}

Con el método Task.continueWithTask, puedes encadenar estas dos tareas:

Task<AuthResult> signInTask = FirebaseAuth.getInstance().signInAnonymously();

signInTask.continueWithTask(new Continuation<AuthResult, Task<String>>() {
    @Override
    public Task<String> then(@NonNull Task<AuthResult> task) throws Exception {
        // Take the result from the first task and start the second one
        AuthResult result = task.getResult();
        return doSomething(result);
    }
}).addOnSuccessListener(new OnSuccessListener<String>() {
    @Override
    public void onSuccess(String s) {
        // Chain of tasks completed successfully, got result from last task.
        // ...
    }
}).addOnFailureListener(new OnFailureListener() {
    @Override
    public void onFailure(@NonNull Exception e) {
        // One of the tasks in the chain failed with an exception.
        // ...
    }
});

Cómo bloquear una tarea

Si tu programa ya se está ejecutando en un subproceso en segundo plano, puedes bloquear el subproceso actual y esperar a que se complete la tarea, en lugar de usar una devolución de llamada:

try {
    // Block on a task and get the result synchronously. This is generally done
    // when executing a task inside a separately managed background thread. Doing this
    // on the main (UI) thread can cause your application to become unresponsive.
    AuthResult authResult = Tasks.await(task);
} catch (ExecutionException e) {
    // The Task failed, this is the same exception you'd get in a non-blocking
    // failure handler.
    // ...
} catch (InterruptedException e) {
    // An interrupt occurred while waiting for the task to complete.
    // ...
}

También puedes especificar un tiempo de espera cuando bloqueas una tarea para evitar que la aplicación se bloquee indefinidamente si la tarea tarda demasiado en completarse:

try {
    // Block on the task for a maximum of 500 milliseconds, otherwise time out.
    AuthResult authResult = Tasks.await(task, 500, TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
    // ...
} catch (InterruptedException e) {
    // ...
} catch (TimeoutException e) {
    // Task timed out before it could complete.
    // ...
}

Interoperabilidad

Task está diseñado para funcionar bien con otros patrones de programación asíncronos comunes de Android. Se puede convertir a otras primitivas, como ListenableFuture y corrutinas de Kotlin, que recomienda AndroidX, lo que te permite usar el enfoque que mejor se adapte a tus necesidades.

A continuación, se muestra un ejemplo con un Task:

// ...
simpleTask.addOnCompleteListener(this) {
  completedTask -> textView.text = completedTask.result
}

Corrutina de Kotlin

Para usar corrutinas de Kotlin con Task, agrega la siguiente dependencia a tu proyecto y, luego, usa el fragmento de código para convertir desde un Task.

Gradle (build.gradle a nivel del módulo, generalmente app/build.gradle)
// Source: https://github.com/Kotlin/kotlinx.coroutines/tree/master/integration/kotlinx-coroutines-play-services
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3'
Fragmento
import kotlinx.coroutines.tasks.await
// ...
  textView.text = simpleTask.await()
}

Guava ListenableFuture

Para usar Guava ListenableFuture con Task, agrega la siguiente dependencia a tu proyecto y, luego, usa el fragmento de código para convertir desde un Task.

Gradle (build.gradle a nivel del módulo, generalmente app/build.gradle)
implementation "androidx.concurrent:concurrent-futures:1.2.0"
Fragmento
import com.google.common.util.concurrent.ListenableFuture
// ...
/** Convert Task to ListenableFuture. */
fun <T> taskToListenableFuture(task: Task<T>): ListenableFuture<T> {
  return CallbackToFutureAdapter.getFuture { completer ->
    task.addOnCompleteListener { completedTask ->
      if (completedTask.isCanceled) {
        completer.setCancelled()
      } else if (completedTask.isSuccessful) {
        completer.set(completedTask.result)
      } else {
        val e = completedTask.exception
        if (e != null) {
          completer.setException(e)
        } else {
          throw IllegalStateException()
        }
      }
    }
  }
}
// ...
this.listenableFuture = taskToListenableFuture(simpleTask)
this.listenableFuture?.addListener(
  Runnable {
    textView.text = listenableFuture?.get()
  },
  ContextCompat.getMainExecutor(this)
)

RxJava2 Observable

Agrega la siguiente dependencia, además de la biblioteca asíncrona relativa que elijas, a tu proyecto y, luego, usa el fragmento de código para convertir desde un Task.

Gradle (build.gradle a nivel del módulo, por lo general, app/build.gradle)
// Source: https://github.com/ashdavies/rx-tasks
implementation 'io.ashdavies.rx.rxtasks:rx-tasks:2.2.0'
Fragmento
import io.ashdavies.rx.rxtasks.toSingle
import java.util.concurrent.TimeUnit
// ...
simpleTask.toSingle(this).subscribe { result -> textView.text = result }

Próximos pasos