sábado, 20 de septiembre de 2014

Ciclo de vida de las actividades.

Cuando interactuamos con una aplicación, sus actividades cambian de estado continuamente. Por ejemplo, cuando una actividad se inicia por primera vez, ésta pasa a un primer plano para interactuar con el usuario. Durante este proceso, Android invoca una secuencia de métodos retrollamada asociados al ciclo de vida de la actividad.  Desde estos métodos, por ejemplo, podremos configurar la interfaz del usuario. Si el usuario realiza alguna acción pasando a iniciar otra actividad, o bien cambia de aplicación, será ésta última la que pase a un primer plano. La actividad inicial pasará a un segundo plano conservándose su estado.

Los métodos retrollamada relacionados con el ciclo de vida de una actividad nos serán útiles para especificar el comportamiento de ésta ante las idas y venidas del usuario. Por ejemplo, si estamos programando un reproductor para vídeo streaming, deberíamos detener la reproducción y cerrar la conexión de red cuando el usuario cambia de aplicación. Si el usuario decide volver al reproductor, volveremos a reestablecer la conexión de red y la reproducción justo donde se había quedado.

Iniciando nuestra actividad.

A diferencia de otros paradigmas de la programación en los que las aplicaciones empiezan su ejecución desde un método principal, el sistema Android puede iniciar una actividad cualquiera de nuestra aplicación, invocando sus métodos retrollamada.

Las retrollamadas del ciclo de vida.

Cada vez que el sistema invoca una secuencia de métodos retrollamada de una actividad, lo hace siguiendo una estructura piramidal.  Cada uno de los estados por los que transita la actividad se correspondería con cada uno de los escalones de la pirámide (ver imagen 1). Cada vez que el sistema inicia una actividad, invoca una secuencia de métodos que la hacen transitar hacia la parte superior de la pirámide (estado reanudada), pasando a un primer plano.

Cuando el usuario comienza a abandonar la actividad, el sistema invocará otra secuencia de métodos que la hacen transitar hacia la parte más baja de la pirámide. El camino de descenso en la pirámide puede ser parcial, es decir, que la actividad pase a un estado de espera (el usuario ha cambiado de aplicación, por ejemplo). Desde un estado de espera, la actividad podría volver a emprender el camino de ascenso en la pirámide (el usuario vuelve a la actividad, por ejemplo) y volver al punto de partida.

Imagen 1. Ciclo de vida de una actividad y su estructura piramidal. Secuencia de llamadas de ascenso por la izquierda. Secuencia de llamadas de ascensos/descensos parciales por la derecha.

No siempre tendremos que implementar todos los métodos retrollamada asociados a una actividad. Esto dependerá de la complejidad de la misma. Sin embargo, es importante saber cómo funcionan y ofrecer al usuario el comportamiento que espera, garantizándole entre otras cosas:
  • Que no se produzca un error en nuestra aplicación mientras recibimos una llamada de teléfono o cambiamos de aplicación.
  • Que no se consuman recursos importantes del sistema mientras no estemos usando la aplicación.
  • Que no se pierda el progreso del usuario si éste sale y entra de la aplicación.
  • Que no se produzcan errores o se pierdan datos cuando se cambie la orientación de la pantalla.

Una actividad podrá estar en uno de en uno de los siguientes tres estados:
Reanudada.
La actividad se muestra en primer plano y el usuario puede interactuar con ella. En ocasiones a este estado también se le puede llamar en ejecución.
Pausada o en pausa.
Aún se puede ver la actividad mientras otra actividad pasa a primer plano y obtiene el foco. Nuestra actividad aún se sigue visualizando aunque cuente con otra actividad por encima de ella que es semi transparente, o bien, no ocupa toda la pantalla. El usuario no podrá interactuar con una actividad en pausa y ésta no podrá ejecutar código alguno.
Detenida.
La actividad pasa a estar oculta en su totalidad por otra actividad, pasando a un segundo plano. Mientras una actividad está detenida, su estado será conservado y no podrá ejecutar código alguno.

Los estados creada e iniciada son estados transitorios de la actividad por los que ésta pasa cuando es ejecutada por primera vez. Es decir, el sistema llama a la secuencia de métodos onCreate(), onStart() y onResume(), pasando la actividad directamente a estar estar en estado reanudada.

Configurando nuestra actividad para que aparezca en el lanzador de aplicaciones.

Toda aplicación cuenta con un icono en el lanzador de aplicaciones desde el que podremos ejecutarla. Cuando ejecutamos una aplicación desde el lanzador de aplicaciones, ésta inicia su actividad principal que será la encargada de mostrar la interfaz del usuario.

Podemos declarar qué actividad va a ser la actividad principal de nuestra aplicación desde en el archivo de configuración AndroidManifest.xml en el directorio raíz de nuestro proyecto. Para ello utilizaremos un filtro de intenciones especial (<intent-filter>), donde la acción será de tipo MAIN y la categoría de tipo LAUNCHER:
<activity android:name=".MainActivity" android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
Nota.Cuando creamos un proyecto haciendo uso de las herramientas del SDK, por defecto se crea una actividad principal con este filtro.

Si el filtro anterior no se encuentra declarado en ninguna de las actividades de nuestra aplicación, ésta no aparecerá en el lanzador de aplicaciones.

Creando una nueva instancia.

La mayoría de las aplicaciones incluyen múltiples actividades permitiendo al usuario realizar múltiples operaciones. Cada vez que el usuario inicia una actividad, el sistema crea una nueva instancia de ella e invoca su método onCreate(). En éste, implementaremos operaciones básicas que deben ejecutarse tan solo una vez a lo largo de toda la vida útil de la actividad. Por ejemplo, es el lugar adecuado para definir la interfaz del usuario y sus variables miembro asociadas.

En el siguiente ejemplo, el método onCreate() realiza algunas operaciones para configurar la actividad: declara la interfaz del usuario (definida en una plantilla XML), declara algunas variables miembro y configura parte de la interfaz del usuario:
 TextView mTextView; // Variable miembro que mostrará un texto en la plantilla

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Asignamos la plantilla a la actividad
    // El archivo de la plantilla se encuentra en res/layout/main_activity.xml
    setContentView(R.layout.main_activity);
    
    // Iniciamos la variable miembro mTextView
    mTextView = (TextView) findViewById(R.id.text_message);
    
    // Nos aseguramos de que el sistema es Honeycomb o superior
    // para poder usar la barra de acciones
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        // Hacemos que el icono de la actividad principal NO se comporte como un botón
        ActionBar actionBar = getActionBar();
        actionBar.setHomeButtonEnabled(false);
    }
}
Nota. La constante Build.VERSION.SDK_INT se encuentra disponible a partir de la versión 2.0 de Android (versión 5 de la API).

Una vez finalizada la ejecución del método onCreate(), el sistema llama a los métodos onStart() y onResume(). La actividad nunca estará en los estados creada o iniciada, pasando directamente al estado reanudada y permaneciendo en él hasta que sucede algo: una llamada telefónica, el usuario cambia de actividad o la pantalla entra en reposo.

Los métodos onStart() y onResume() nos serán útiles para controlar cuándo la aplicación pasa al estado reanudada habiendo sido previamente pausada o detenida. Este tema lo abordaremos más adelante.

Nota. El método onCreate() incluye un parámetro savedInstanceState que nos servirá para restaurar el estado de una aplicación, respectivamente. Este tema lo abordaremos más adelante.

Imagen 2. Ciclo de vida de una actividad, resaltando la parte en la que ésta es creada.
Secuencia de llamadas: onCreate(), onStart() y onResume().
Una vez finaliza la ejecución de estos métodos, la actividad pasa al estado reanudada,
 donde el usuario podrá interactuar con ella hasta que cambie a otra actividad.

Destruyendo nuestra actividad.

El último de los métodos retrollamada invocado por el sistema dentro del ciclo de vida de una actividad es onDestroy(). Que el sistema llame a este método es sintomático de que la actividad va a ser eliminada de la memoria.

La mayoría de las aplicaciones no necesitan implementar este método porque la mayoría de las referencias locales a la clase se acabarán liberando con la desaparición de la propia actividad. Para realizar tareas de limpieza, los métodos onPause() y onStop() son más apropiados. Sin embargo, si utilizamos hebras (threads) ejecutándose en segundo plano, creados en el método onCreate(), esto podría traducirse en una pérdida de memoria. En este último caso, el método onDestroy() es el más adecuado para acabar con la ejecución de esas hebras.
@Override
public void onDestroy() {
    super.onDestroy();  // Siempre llamaremos al método de la clase padre
    
    // Detenemos la depuración del código iniciada en el método onCreate()
    android.os.Debug.stopMethodTracing();
}
Nota. El sistema llama al método onDestroy() después de llamar a los métodos onPause() y onStop() en todas las situaciones a excepción de una: cuando terminamos la actividad llamando al método finish() dentro del método onCreate(). En este caso, el sistema llama inmediatamente después al método onDestroy(), obviando el resto de métodos.

Pausando y reanudando nuestra actividad.

Durante el uso normal de una aplicación, la actividad que se encuentra en primer plano puede quedar parcialmente oculta por la superposición de otra actividad semi transparente o que no ocupa toda la pantalla (un cuadro de diálogo, por ejemplo). En este caso, nuestra actividad pasará a estar pausada.

Cuando una actividad entra en pausa, el sistema llama a su método onPause(). Esto nos permitirá implementar acciones para detener ciertos procesos (parar la reproducción de un video, por ejemplo) o guardar cierta información de manera persistente si el usuario abandona la aplicación. Cuando el usuario vuelve a la aplicación, el sistema llamará al método onResume().

Nota. Cuando el sistema llama al método onPause() de una actividad, podríamos pensar que después de entrar en pausa durante un tiempo, el usuario volverá a ella. Sin embargo, en la mayoría de los casos, este hecho está más bien relacionado con el abandono de la actividad por parte del usuario.

Imagen 3. Cuando nuestra actividad es atenuada por la superposición de otra actividad:
[1] El sistema llamará al método onPause() de la actividad y ésta pasará a estar en pausa.
[2] El sistema llamará al método onResume() y la actividad pasará a estar reanudada si el usuario decide volver a la actividad.

Pausando nuestra actividad.

Cuando el sistema llama al método onPause() de nuestra actividad, técnicamente significa que ésta pasa a ser parcialmente visible, pero en la mayoría de los casos esto no será así y lo que realmente nos indica, es que el usuario estaría abandonando la actividad y que ésta cambiará su estado a detenida. Deberíamos reservar el uso del método onPause() para:
  • Detener animaciones o cualquier otro tipo de operaciones que hagan un uso intensivo de la CPU.
  • Guardar los datos que el usuario espera sean permanentes (el borrador de un correo electrónico, por ejemplo).
  • Liberar recursos del sistema tales como: receptores de avisos, sensores (como el GPS), o cualquier otro recurso que pueda afectar al consumo de la batería.

Por ejemplo, si nuestra aplicación usa la cámara, el método onPause() es un buen sitio para pasar a liberarla para que otras actividades puedan hacer uso de ella:
@Override
public void onPause() {
    super.onPause();  // Siempre hay que llamar al método de la clase padre

    // Liberamos la cámara porque no la necesitaremos mientras estemos en pausa
    // y otras actividades podrían necesitarla.
    if (mCamera != null) {
        mCamera.release()
        mCamera = null;
    }
}

Como norma, no deberíamos usar el método onPause() para almacenar cualquier cambio hecho por el usuario (cambios en los campos de un formulario, por ejemplo), salvo que el usuario espere que se haga un guardado automático; como cuando se redacta un correo electrónico y éste se guarda como borrador. Deberíamos evitar hacer operaciones que hagan un uso intensivo de CPU en este método, como podrían ser las operaciones de escritura sobre una base de datos, porque esto ralentiza visualmente la transición hacia otras actividades (utiliza el método onStop() en su lugar).

Nota. Cuando nuestra actividad entra en pausa, su instancia se conservará en memoria y seguirá disponible cuando ésta sea reanudada. No necesitaremos volver a definir los componentes que fueron creados durante cualquiera de los métodos previos a la reanudación.

Reanudando nuestra actividad.

Cuando el usuario reanuda una actividad pausada, el sistema invoca su método onResume(). Ten en cuenta que el sistema llama a este método cada vez que nuestra actividad entra en primer plano, incluyendo cuando es creada por primera vez. Deberíamos usar este método para reactivar componentes que previamente desactivamos en el método onPause() (activar las animaciones y otros componentes usados mientras la actividad tiene el foco).

En el ejemplo siguiente, el método onResume() hace justo lo contrario al método onPause():
@Override
public void onResume() {
    super.onResume();  // Siempre llamamos al método de la clase padre

    // Inicializamos la cámara si su instancia es null
    if (mCamera == null) {
        initializeCamera(); // Método local encargado de inicializar la cámara
    }
}

Deteniendo y reiniciando nuestra actividad.

Detener y reiniciar nuestra actividad es un proceso importante que nos asegura que los usuarios perciban nuestra aplicación como que siempre está viva y que no van a perder su trabajo. Hay unos pocos escenarios en los que nuestra actividad será detenida y reiniciada:
  • El usuario cambia a una aplicación de la lista de tareas recientes, pasando nuestra actividad a ser detenida. Si el usuario vuelve a nuestra aplicación a través del lanzador de aplicaciones, o a través de la lista de tareas recientes, ésta será reiniciada.
  • El usuario realiza una acción desde la propia aplicación que inicia una nueva actividad. La actividad actual será detenida mientras que la segunda será creada. Si el usuario pulsa el botón de retroceso, la primera actividad será reiniciada.
  • El usuario recibe una llamada telefónica mientras interactúa con la aplicación en su teléfono.

La clase Activity proporciona dos métodos, onStop() y onRestart(), para gestionar nuestra actividad en los escenarios descritos. A diferencia del estado en pausa, que se caracteriza por la ocultación parcial de nuestra actividad, el estado detenida se caracteriza porque esta ocultación es completa y el usuario estará ocupado con otra actividad.

Nota. Debido a que el sistema mantiene la instancia de la actividad en memoria cuando ésta es detenida, es posible que no necesitemos implementar los métodos onStop() y onRestart() (o incluso onStart()). La mayoría de las actividades son relativamente simples, la actividad se detendrá y se reiniciará correctamente y solo necesitamos el método onPause() para detener ciertas acciones y liberar recursos del sistema.

Imagen 4. Cuando el usuario deja nuestra actividad, el sistema llama al método onStop() [1]. Si el usuario vuelve mientras la actividad está detenida, el sistema llama al método onRestart() [2], seguido de onStart() [3] y onResume() [4]. Observa que no importa cuál sea la causa que llevó que detuvo la actividad, el sistema siempre llamará al método onPause() antes que al método onStop().

Deteniendo nuestra actividad.

Cuando el sistema llama al método onStop(), la actividad se oculta. Es en este momento cuando deberíamos liberar todos los recursos que no vayan a ser utilizados por el usuario. Mientras la actividad está detenida, el sistema podría eliminarla si anda escaso de memoria. En situaciones límite, el sistema puede destruir nuestra aplicación sin llegar a invocar el método onDestroy(), por ello deberíamos usar el método onStop() para liberar recursos y así evitar pérdidas de memoria.

Aunque el método onPause() se llama antes que el método onStop(), usaremos el método onStop() para realizar operaciones voluminosas de escritura sobre una base de datos.

Una implementación del método onStop() que guarda el contenido de una nota borrador, podría ser la siguiente:
@Override
protected void onStop() {
    super.onStop();  // Siempre se llama al método de la clase padre

    // Guardamos la nota borrador porque la actividad va a ser detenida
    // y queremos asegurarnos de que los cambios no se pierden
    ContentValues values = new ContentValues();
    values.put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText());
    values.put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle());

    getContentResolver().update(
            mUri,    // El URI de la nota a actualizar.
            values,  // Nombres de columna y sus valores actualizados.
            null,    // No utilizamos cláusula SELECT.
            null     // No utilizamos cláusula WHERE.
            );
}

Cuando la actividad es detenida, ésta permanece en memoria y se reutiliza cuando la actividad pasa a ser reanudada. No necesitas reiniciar componentes que fueron creados durante métodos previos a la reanudación. El sistema también mantiene el estado actual para cada una de las vistas de la plantilla y si el usuario introdujo información en un campo de texto, por ejemplo, su contenido será conservado y no tendremos que implementar ningún mecanismo adicional.

Nota. Aún si el sistema destruye nuestra actividad mientras ésta está detenida, el estado de las vistas se seguirá conservando en un objeto Bundle.

Iniciando/reiniciando nuestra actividad.

Cuando nuestra actividad vuelve al primer plano habiendo estado detenida, el sistema primero invoca su método onRestart(). A continuación, el sistema llama al método onStart() y la aplicación empieza a hacerse visible (cosa que sucede tanto si ha sido reiniciada como si ha sido creada por primera vez). El método onRestart() será invocado solo cuando la actividad se reanuda desde el estando detenida.

No es común que una aplicación necesite usar el método onRestart() para recuperar parte del estado de una actividad. Debido a que el método onStop() libera todos los recursos de nuestra actividad, necesitaremos recuperarlos cuando la actividad sea reiniciada. Por otro lado, también será necesario crear una instancia de esos mismos recursos cuando la actividad se crea por primera vez. Es por esta razón, por la que deberíamos usar el método onStart() como el método homólogo al método onStop().

Por ejemplo, cuando el usuario vuelve a nuestra aplicación desde otra aplicación, el método onStart() será un buen sitio para verificar que los recursos requeridos aún se encuentran activos:
@Override
protected void onStart() {
    super.onStart();  // Siempre llamamos al método de la clase padre.
    
    // Tanto si la actividad se inicia por primera vez como si se ha reiniciado
    // comprobaremos si el GPS se encuentra activo.
    LocationManager locationManager = 
            (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    boolean gpsEnabled =
            locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
    
    if (!gpsEnabled) {
        // Creamos un cuadro de diálogo aquí que solicite al usuario que active el GPS.
        // Usamos una intención del tipo android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS.
        // Esto muestra el panel de configuración del GPS al usuario para que pueda activarlo.
    }
}

@Override
protected void onRestart() {
    super.onRestart();  // Siempre llamamos al método de la clase padre.
    
    // La actividad reinicia desde el estado detenida.   
}

Cuando el sistema destruye nuestra actividad, llama a su método onDestroy(). Debido a que deberíamos liberar casi todos nuestros recursos desde el método onStop(), no hay mucho más que hacer desde el método onDestroy(). Este método es nuestra última posibilidad para liberar recursos que podrían ocasionar pérdidas de memoria: hebras adicionales que hayamos creado, trazas de depuración del código, conexiones a bases de datos, etc.

Recreando nuestra actividad.

Hay algunos escenarios en los que una actividad es destruida durante el uso habitual de nuestra aplicación: cuando el usuario pulsa el botón de retroceso, o cuando nuestra actividad se destruye así misma invocando el método finish(). El sistema también eliminará nuestra actividad si se encuentra detenida y no ha sido usada durante un largo período de tiempo, o si la actividad en primer plano require de más recursos y el sistema tiene que recurrir a cerrar procesos en segundo plano para recuperar memoria.

Tanto si nuestra actividad se destruye porque el usuario pulsa el botón de retroceso, como si se destruye por llamar al método finish(), la instancia de la actividad también será destruida con ello. Aunque la instancia se haya destruido debido a las limitaciones del sistema, si el usuario vuelve a la actividad, el sistema volverá a recrear su instancia a partir de los datos previamente almacenados a su destrucción. Estos datos, datos de estado, se almacenan en una colección de pares clave-valor de tipo Bundle.

Importante. Nuestra actividad será destruida y recreada cada vez que cambiemos la orientación de la pantalla de nuestro dispositivo. El sistema actúa de esta manera para que la actividad pueda adaptarse a la nueva configuración cargando recursos alternativos (por ejemplo, una nueva plantilla optimizada para cierta orientación de la pantalla).

Por defecto, el sistema usa un objeto de tipo Bundle para guardar la información asociada a las vistas de una plantilla (como podría ser un campo de texto EditText). De esta manera, si nuestra actividad es destruida y recreada, la plantilla volverá a recuperar su estado previo.

Nota. Para que el sistema Android sea capaz de recuperar un estado previo de las vistas de una plantilla, será necesario que éstas dispongan de un identificador único, es decir, un valor para el atributo android:id.

Para almacenar nuestros propios datos de estado, utilizaremos el método retrollamada onSaveInstanceState(). El sistema llama a este método cuando el usuario abandona la actividad, pasándole como argumento un objeto de tipo Bundle en el que se almacena el estado de la actividad. Si el sistema recrea la actividad más tarde, pasa el mismo objeto Bundle tanto a los métodos onRestoreInstanceState() y onCreate().

Eliminación y recuperación de una actividad
Imagen 5. Justo antes de que nuestra actividad se detenga, se invoca el método onSaveInstanceState() [1] y podremos especificar información adicional que necesitaremos más tarde cuando la actividad vuelva a recuperarse. Si la actividad se destruye y posteriormente es recreada, el sistema pasa el estado definido en [1] tanto al método onCreate() [2] como al método onRestoreInstanceState() [3].

Guardando el estado de nuestra actividad.

Cuando nuestra actividad comienza a detenerse, el sistema invoca el método onSaveInstanceState() y nuestra actividad guarda su información de estado. La implementación por defecto de este método, guarda información referente a las vistas, como el texto introducido en un campo de texto EditText, o la posición del desplazamiento de una lista ListView.

Para guardar información extra de nuestra actividad, debemos implementar el método onSaveInstanceState() y añadir los pares clave-valor que necesitemos en el objeto Bundle. Por ejemplo:
static final String STATE_SCORE = "playerScore";
static final String STATE_LEVEL = "playerLevel";
...

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    // Guardamos los datos del usuario referentes al juego
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
    savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);
    
    // Siempre llamaremos al método de la clase padre
    super.onSaveInstanceState(savedInstanceState);
Importante. Llama siempre al método de la clase padre para que la implementación por defecto tenga efecto y se almacene el estado de las vistas.

Restaurando el estado de nuestra actividad.

Cuando nuestra actividad es recreada después de haber sido destruida, puede recuperar su estado original a partir del objeto Bundle proporcionado tanto por su método onCreate(), como por su método onRestoreInstanceState().

Puesto que el método onCreate() se invoca tanto para crear la actividad por primera vez, como cuando ésta es recreada, tendremos que comprobar si el objeto Bundle es nulo o no antes de consultar su contenido. Si es nulo, es porque el sistema está creando por primera vez la actividad y ésta no necesita restaurar nada:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); // Always call the superclass first
   
    // Check whether we're recreating a previously destroyed instance
    if (savedInstanceState != null) {
        // Restore value of members from saved state
        mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
        mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
    } else {
        // Probably initialize members with default values for a new instance
    }
    ...
}
Si no quieres tener que comprobar si el objeto Bundle es nulo o no, podemos usar el método onRestoreInstanceSatate(). El sistema invoca este método solo si se produce una recuperación del estado de la actividad, por lo que el objeto Bundle siempre tendrá datos:
public void onRestoreInstanceState(Bundle savedInstanceState) {
    // Siempre hay que llamar al método de la clase padre
    super.onRestoreInstanceState(savedInstanceState);
   
    // Restauramos variables miembro de la actividad
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
}
Importante. Tendremos que invocar siempre al método de la clase padre si queremos recuperar el estado previo de las vistas.

No hay comentarios:

Publicar un comentario