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:
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.
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).
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()
#2. Se crea una nueva instancia en la tarea si no está en la parte superior.
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()
.
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.
Si intentas crear actividades adicionales en la tarea donde está D, estas irán a otra tarea:
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
:
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:
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:
Puntos a tener en cuenta:
- 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.
- 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:
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 :
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.