Actividades En Android 2: Task Y Back Stack

En este tutorial veremos como Android usa tasks y back stacks para agrupar las actividades de nuestras apps. Lo que para el usuario representa la realización de un trabajo específico usando la barra de navegación ( Back, Home y Overview).

App Android De Ejemplo

Para que podamos ver el comportamiento en tiempo real del código que estudiemos he creado una app que simula la posición de las actividades en sus tareas.

La idea es que al presionar un botón se ejecute la actividad asociada a su texto y la lista agregue el ítem en tiempo real. Igual al momento de navegar hacia atrás ver como se eliminan los elementos

¿Qué Es Una Tarea En Android?

Una tarea (task) es una colección de actividades en una app con las que el usuario tiene interacción.

Para cada tarea Android usa una pila (back stack) LIFO como mecanismo de registro para recordar que la posición en que las actividades fueron creadas.

Por ejemplo: supongamos que tenemos una app de chats como WhatsApp y deseamos ver la lista de chats, ir a un chat especifico y luego ver el detalle del contacto.

Si cada una de estas pantallas fuera una actividad, la representación lógica de la task y su back stack en cada interacción de usuario serían la siguiente:

Las actividades ámbar oscuro representarán la actividad en primer plano

Navigation Bar Y Tasks

Una de las formas más amigables en que el usuario puede intercambiar y eliminar tareas es usando la barra de navegación de Android.

Los botones Back, Home y Overview le permiten sentir el control del sistema para llevar a cabo sus propósitos.

Es por esto que veremos cómo esta barra puede afectar a las actividades y tareas.

El Back Button

El botón Volver es la interfaz para navegar hacia atrás. Esto quiere decir que remueve la actividad de la parte superior (TOS) de la tarea en primer plano.

Retomando el ejemplo anterior. Supongamos que ya no deseamos ver el detalle del contacto y deseamos chatear con el contacto. Al presionar Back tendremos:

Home Button Y Tasks En Segundo Plano

Envía la tarea actual a segundo plano conservando la pila intacta y hace visible la pantalla del home. Lo que significa pasar a primer plano la tarea del Launcher.

Un ejemplo de esto sería al presionar Home estando en nuestra app de chats y luego iniciar nuestra app de correos electrónicos:

Overview Button Y El Intercambio Entre Tareas

Inicia una sección de UI llamada pantalla de recientes. Aquí se muestran en recuadros flotantes las tareas (la miniatura es la de su actividad en la parte superior) que se han iniciado en orden de acceso.

Esta utilidad para el usuario le permite swipear horizontalmente para elegir una tarea que traer el foreground, swiper verticalmente para eliminar la tarea y eliminar todas las tareas si así lo desea.

Vistazo rápido de la Overview Screen en Android 9

Gestionar Tareas Programáticamente

Existen casos específicos donde requeriremos manipular la forma en que una actividad es apilada o retirada en la back stack de una tarea .

Es por eso que el framework nos provee atributos del Manifest o banderas de la clase Intent para dicho fin como veremos a continuación.

Modificar Modos De Lanzamiento En El Android Manifest

Desde el archivo AndroidManifest.xml usaremos el atributo launchMode para especificar el modo de lanzamiento de una actividad.

Sus posibles valores son los siguientes: standard, singleTop, singleTask y singleInstance.

Veamos el comportamiento de Android con cada valor…

Launch Mode Standard

El valor por defecto para launchMode es standard .

<activity
    android:name=".ui.acts.StandardActivity"
    android:launchMode="standard" />

La actividad se creará dentro de la tarea desde donde fue iniciada y se puede generar más de una instancia (en cualquier tarea).

Al hacer (A) > (A) > (A) resulta t187=[0, A, A, A]

Launch Mode Single Top

singleTop evita que el sistema cree una nueva instancia de la actividad si actualmente existe una en la parte superior de la pila.

#1. Si existe una instancia en la parte superior de la back stack de la actividad, entonces se evita su creación y se llama a su método onNewIntent()

Hacer (B) > (B) produce t187=[0, B*]

#2. Se crea una nueva instancia en la tarea si no está en la parte superior.

Hacer (B) > (A) > (B) produce t187=[0, B, A, B]

Launch Mode Single Task

El valor singleTask hace que se cree una nueva tarea y se añada como raíz la actividad (solo si defines una afinidad).

<activity
    android:name=".ui.acts.SingleTaskActivity"
    android:launchMode="singleTask" />

Si ya existe una instancia de la actividad en la tarea se pone en la parte superior de la pila (destruyendo las que estén por encima de ella) y se llama a onNewIntent().

Hacer (C) > (A) > (C) produce t188=[0, C*]

Launch Mode Single Instance

En el caso de singleInstance es semejante a singleTask, solo que la actividad será el único miembro en la tarea por lo que lanzara actividades en una tarea separada.

<activity
    android:name=".ui.acts.SingleInstanceActivity"
    android:launchMode="singleInstance" />

Se comporta igual que singleTask, solo que el sistema evita que se creen otras actividades en la tarea. Por eso se crea una nueva tarea para aislar la instancia.

Hacer (D) > (D) produce t189=[D*] | t188=[0]

Si intentas crear actividades adicionales en la tarea donde está D, estas irán a otra tarea:

Hacer (A) > (A) produce
t188=[0, A, A] | t189=[D]

Modificar El Modo De Lanzamiento Con Las Banderas De Intent

Cuando iniciamos una actividad a través del método startActivity() es posible pasar banderas de la clase Intent que pueden sobrescribir el valor de launchMode a través del método addFlags().

Veamos las principales:

FLAG_ACTIVITY_NEW_TASK

Crea una tarea nueva con la actividad como raíz (solo si existe una afinidad):

Intent intent = new Intent(this, FlagsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(flags);
startActivity(intent);

En la sección de afinidades veremos cómo funciona.

FLAG_ACTIVITY_SINGLE_TOP

Produce el mismo resultado que singleTop en launchMode:

Intent intent = new Intent(this, FlagsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.addFlags(flags);
startActivity(intent);

FLAG_ACTIVITY_CLEAR_TOP

Si la actividad existe en la tarea actual, se destruyen todas las demás actividades arriba de ella, dejándola en la parte superior.

Intent intent = new Intent(this, FlagsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(flags);
startActivity(intent);

Se podría interpretar que FLAG_ACTIVITY_CLEAR_TOP produce el efecto de limpieza que vimos en singleTask:

Hacer (E) > (A) > (A) > (E:clear_top)
produce t190=[0, E]

Afinidad De Una Actividad

La afinidad es la tarea a la cual una actividad prefiere pertenecer. Por defecto todas las actividades de una app comparten la misma afinidad.

Esta es la razón del porque singleTask y FLAG_ACTIVITY_NEW_TASK no crean una nueva tarea sin tener afinidad definida, ya que obtarán por crearse en la tarea inicial que tiene la misma afinidad .

Para definir una nueva afinidad usa el atributo taskAffinity. El valor puedes crearlo extendiendo el nombre del paquete del proyecto.

Veamos:

#1. Marcar una actividad que se inicie como nodo principal de una nueva tarea con el id de la afinidad en el atributo taskAffinity .

<activity
    android:name=".ui.acts.FlagsActivity"
    android:taskAffinity=".task1" />

#2. Crear un nuevo intent y añadirle la bandera FLAG_ACTIVITY_NEW_TASK con addFlags(). Iniciar la actividad con startActivity():

Intent intent = new Intent(this, FlagsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

Con la app de ejemplo podemos simular este comportamiento iniciando E con la bandera y ver como se crea una nueva tarea, para añadirle actividades A:

Hacer (E:new_task) > (A) > (A) produce
t192=[E, A, A] | t190=[0]

Si presionas el botón Overview verás que habrán dos recuadros para cada tarea de la app:

Dos tareas de una misma aplicación en la Recientes

Puntos a tener en cuenta:

  1. Si presionas Home en tu nueva tarea y luego presionas el icono de la app, se abrirá la tarea principal y no la que abandonaste.
  2. Usar el botón Overview para cambiar entre las tareas de tu misma app hace que la tarea del home se traiga a primer plano al agotar las actividades de la tarea actual.

Comprendiendo esto, si hay una navegación dependiente entre las actividades que hay en ambas tareas, entonces deberías asegurarte de que no pierdan contacto al presionar el Back button.

Ya sea usando un view para restaurarla (como los botones de la app de simulación) ó sobrescribiendo el método del Back button:

@Override
public void onBackPressed() {    
    // Recrear dependencia de navegación
}

De esta forma, si reanudas una tarea de tu app en el background y luego accedes a ella desde otra, su back stack se apilará en la actual. Permitiéndonos realizar la navegación deseada.

Limpiar La Pila De Actividades

Android limpia automáticamente la pila de actividades si ha detectado que el usuario abandono la app por un tiempo prolongado. El único elemento que se conserva es la raíz de la pila.

Sin embargo, existen atributos de <activity> que te permiten cambiar este comportamiento si así lo piden tus casos de uso de tu proyecto. Veamos:

alwaysRetainTaskState

Asigna el valor de true en la actividad raíz de la tarea para que se conserven las demás actividades luego de un tiempo prolongado (30 minutos aproximadamente).

<activity
    android:name=".ui.acts.SingleInstanceActivity"
    android:alwaysRetainTaskState="true"
    android:launchMode="singleInstance" />

clearTaskOnLaunch

Asigna true a la actividad raíz para que la pila sea limpiada cuando el usuario presione presione Home y reanude la tarea con el icono de la app.

<activity
    android:name=".ui.acts.MainActivity"
    android:clearTaskOnLaunch="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Cada actividad iniciada se destruiría al ir al home y regresar:

Hacer (A) > (A) > Home > Icon produce
t196=[0]

finishOnTaskLaunch

Informa al sistema terminar a la actividad marcada cuando se es presionado el botón Home y se reanuda la tarea desde el Launcher :

Hacer (A) > (F) > (A) > Home > Icon
produce t197=[0, A, A]

Iniciar Una Tarea

La forma de iniciar una tarea para nosotros los desarrolladores Android es a través de un Intent Filter en la actividad principal de nuestros proyectos.

Este elemento permite definir al nodo principal en la back stack con que se iniciará la tarea.

La idea es proveer en el <intent-filter> el valor "android.intent.action.MAIN" para la acción y "android.intent.category.LAUNCHER" para la categoría.

<activity android:name=".ui.acts.MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

El filtro permite la creación del icono de la app en el Launcher con el fin de iniciar la tarea desde cero o reanudarla si ya ha sido creada.

Descargar Código De La App

Descarga el proyecto Android Studio completo mientras a la misma vez me apoyas con la escritura de futuros tutoriales. ¡Realmente agradecería tu ayuda!

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!