domingo, 28 de septiembre de 2014

Gestionando cambios de configuración en tiempo de ejecución

La configuración de un dispositivo suele cambiar durante la ejecución de nuestra aplicación: orientación de la pantalla, disponibilidad del teclado, idioma, etc. Cuando uno de estos cambios sucede, el sistema reinicia la actividad que se está ejecutando justo en ese instante (se invocan sus métodos onDestroy y onCreate(), por ese orden). Durante este proceso, el sistema también actualiza los recursos con aquellos que mejor se adaptan a la nueva configuración.

Para gestionar de manera adecuada el reinicio de nuestra aplicación, será necesario implementar sus actividades para que éstas sean capaces de recuperar sus estados previos. Como norma general, utilizaremos el método onSaveInstanceState() para guardar los datos necesarios y utilizaremos los métodos onCreate() o onRestoreInstanceState() para recuperarlos posteriormente.

Sin embargo, en determinadas ocasiones, restaurar un estado previo de nuestra aplicación puede llegar a ser una operación demasiado costosa dando lugar a una mala experiencia de usuario. En situaciones en las que el rendimiento es prioritario, contamos con dos posibles soluciones:
  • Conservar el estado ante cambios de configuración. La actividad se reiniciará durante un cambio de configuración pero conservando su estado en un objeto externo.
  • Gestionar los cambios de configuración de manera explícita. Impedimos que la actividad se reinicie y pasamos a gestionar el cambio desde un método especial que el sistema llama cada vez que se produce un cambio en la configuración.

Conservar el estado ante cambios de configuración.

Si cada vez que se reinicia nuestra actividad, ésta necesita conservar un gran volumen de datos, o reestablecer una conexión de red, o realizar cualquier otra operación igualmente costosa, podría llegar a ralentizar nuestra aplicación. Por otro lado, guardar el estado en un objeto de tipo Bundle implica limitaciones a la hora de almacenar datos con cierto volumen como puede ser el de una imagen de mapa de bits.

Una solución al problema consiste en utilizar un fragmento como almacén temporal de la información que deseemos conservar para la actividad. Cada vez que el sistema reinicie nuestra actividad, el fragmento seguirá conservando la información previamente almacenada e inalterable.

Para añadir un fragmento de tales características, seguiremos los siguientes pasos:
  1. Creamos un fragmento extendiendo la clase Fragment.
  2. Llamamos al método setRetainInstance(true) en el evento onCreate() del fragmento. Este método conseguirá desligar el fragmento del ciclo de vida de la actividad.
  3. Añadimos el fragmento a nuestra actividad.
  4. Utilizaremos la clase FragmentManager para acceder a los datos del fragmento.

El fragmento quedaría:
public class RetainedFragment extends Fragment {

    // El objeto que queremos conservar.
    private MyDataObject data;

    // Este método solo será llamado una vez.
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // No queremos que el fragmento se reinicie ante cambios de configuración.
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}
Importante. Los objetos que almacenados no deben ser referencias a objetos asociados a la actividad, ni al contexto (imágenes, vistas, adaptadores, etc). Si no tenemos en cuenta este hecho, el recolector de basura no podría recuperar la memoria para dichos objetos.

Usando el fragmento desde la actividad:
public class MyActivity extends Activity {

    private RetainedFragment dataFragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // Localizamos el fragmento en la vista actual.
        FragmentManager fm = getFragmentManager();
        dataFragment = (DataFragment) fm.findFragmentByTag(“data”);

        // Si no se ha encontrado el fragmento, lo añadimos a la actividad.
        if (dataFragment == null) {
            // Añadimos el fragmento.
            dataFragment = new DataFragment();
            fm.beginTransaction().add(dataFragment, “data”).commit();
            // Cargamos los datos desde la Web.
            dataFragment.setData(loadMyData());
        }

        // Podemos acceder a los datos desde dataFragment.getData().
        ...
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // Guardamos los cambios en el fragmento.
        dataFragment.setData(collectMyLoadedData());
    }
}

Gestionar los cambios de configuración de manera explícita.

Si no necesitamos que nuestra aplicación cargue recursos alternativos asociados a cambios en la configuración y además necesitamos evitar que nuestra actividad sea reiniciada por razones de rendimiento, desactivaremos este comportamiento por defecto y pasaremos a gestionarlo nosotros mismos.

Nota. Que los cambios de configuración sean gestionados de manera explícita implica una dificultad añadida si nuestra actividad require de recursos adicionales en función de la configuración. Esta técnica la deberíamos tener en cuenta solo en casos de absoluta necesidad.

Podemos desactivar el mecanismo de reinicio de una actividad desde el archivo manifest utilizando el atributo android:configChanges. Este atributo puede tener varios valores, siendo los más habituales: orientation para evitar que se reinicie la actividad si la orientación de la pantalla cambia y keyboardHidden, para evitar que se reinicie la actividad en función de la disponibilidad del teclado. Para especificar multiples valores del atributo, utilizaremos el carácter | como separador.

Por ejemplo, si queremos que nuestra actividad no se reinicie ante cambios en la orientación y disponibilidad del teclado:
<activity android:name=".MyActivity"
          android:configChanges="orientation|keyboardHidden"
          android:label="@string/app_name">
Ahora cuando se produzca un cambio en la configuración para los valores especificados, la actividad MyActivity no será reiniciada por el sistema. En su lugar, el sistema llamará al método onConfiguracionChanged() de la actividad MyActivity. A este método se le pasa como argumento un objeto de tipo Configuration en el que se puede consultar la nueva configuración y actuar en consecuencia. Antes de llamar a este método, el sistema actualiza el objeto Resources para facilitarnos las operaciones de reajuste que necesitemos realizar.

Importante. A partir de la versión 3.2 de Android (versión 13 de la API), un cambio en la orientación de pantalla lleva implícito un cambio en el tamaño de pantalla. Si queremos evitar el reinicio de nuestra actividad cuando la orientación de pantalla cambia, además, tendremos que añadir el valor screenSize junto al valor orientation en el archivo manifest. Es decir, tendremos que declarar android:configChanges="orientation|screenSize".

En la siguiente secuencia de código utilizamos el método onConfigurationChanged() para mostrar un mensaje al usuario ante un cambio en la orientación:
@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Si la orientación es horizontal, mostramos el mensaje "horizontal" al usuario.
    // Si la orientación es vertical, mostramos el mensaje "vertical" al usuario.
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "horizontal", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "vertical", Toast.LENGTH_SHORT).show();
    }
}
El contenido del objeto Configuration almacena información sobre la configuración actual, incluyendo la orientación de la pantalla. Los valores contenidos en el objeto Configuration son constantes que se encuentran definidas en la clase Configuration (Configuration.ORIENTATION_LANDSCAPE, por ejemplo).

Recuerda. Cuando somos nosotros los encargados de gestionar los cambios de configuración en nuestras actividades, somos responsables de gestionar qué hacer con los recursos alternativos. Si hemos declarado imágenes distintas en función de la orientación de la pantalla por ejemplo, seremos los encargados de realizar los cambios pertinentes desde el método onConfigurationChanged(). Si no tienes que realizar cambios adicionales, no tienes por qué usar este método, en cuyo caso, se seguirán usando los mismos recursos antes del cambio de configuración.

No hay comentarios:

Publicar un comentario