Ejercicio Intermedio C: Preferencias en Android
A. Descripción
Con este ejercicio, veremos cómo utilizar preferencias en Android. Las preferencias son un recurso muy útil y muy valorado por los usuarios de las aplicaciones. Crearemos una preferencia de texto, la cual mostraremos en la pantalla principal de nuestra aplicación.
B. Implementación
Al igual que muchísimas otras partes de la implementación en Android, en las preferencias también se utilizan archivos XML para facilitar la creación de las mismas.
Para crear nuestra propia pantalla de preferencias, simplemente tenemos que crear una actividad que herede de PreferenceActivity, y desde el método onCreate() de
la Actividad cargaremos las preferencias, que no será más que un fichero XML que contiene la
estructura, claves y objetos de la pantalla de preferencias.
El ejercicio finalmente deberá quedar de la siguiente manera:
C. Pasos a seguir
1. Crear un nuevo proyecto Android en Eclipse.
2. Creamos una nueva clase Preferencias que herede de PreferenceActivity.
3. Creamos un fichero XML de preferencias (normalmente en “res/xml/”), el cual llamaremos settings.xml, y el tipo será “Preference”.
4. Añadiremos al fichero de preferencias una categoría llamada “Ajustes básicos”, con una clave (la que queramos) y una descripción. Dentro de ésta categoría incluiremos
una EditTextPreference, que es la que nos permite insertar alguna cadena de
texto en la preferencia (a la cual le deberemos dar también un título, descripción y clave).
5. Añadimos una opción en el menú para realizar la llamada a las preferencias de nuestro programa. Recordar que las preferencias no serán más que otra Actividad.
6. Mediante un objeto SharedPreferences obtenemos las preferencias del programa de la siguiente forma:
SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this);
7. Obtenemos la preferencia de texto mediante su clave. Lo normal es que esa clave sea un string público en la clase de las preferencias.
String texto = p.getString( CLAVE , "Ninguna Preferencia");
8. Mostramos la preferencia en el TextView de la actividad principal.
“Desarrollo de Aplicaciones Móviles en Android”
Ejercicio Intermedio E: Rotación
A.Descripción
Parece que ya llevamos mucho tiempo con esto, pero la verdad es que hay un lado de
los smartphones (en concreto los que llevan Android como sistema operativo) que no hemos descubierto, y ésos son los sensores de movimiento. Estos sensores detectan cómo estamos utilizando nuestro dispositivo en las manos, y son capaces de saber si están boca arriba, boca
abajo, siendo usados de lado (landscape) o de pie (portrait). Por supuesto, hay más
cosas, pero no nos precipitemos.
El objetivo de este ejercicio es primero entender el principio de la rotación. Hasta ahora, hemos estado haciendo aplicaciones destinadas a ser usadas de forma que la pantalla del teléfono sea más alta que ancha (de pie o portrait), que es como hemos utilizado nuestros teléfonos desde siempre. Sin embargo, hay ocasiones en las que querríamos utilizar nuestros teléfonos de lado, de forma que la pantalla sea más ancha que alta. Un ejemplo sería para ver un videoclip, o incluso para poder jugar a un videojuego. Una vez hayamos entendido
el principio de la rotación, nuestro objetivo será adaptarnos a él.
Android soporta la rotación, y es más, la maneja de forma automática. Es decir, sin nosotros hacer ningún cambio en nuestras aplicaciones, éstas ya soportan funcionar en landscape en vez de en portrait. Veámoslo.
B. Implementación
Comenzaremos descargándonos el ejercicio inicial del Subversion. Al abrirlo y
ejecutarlo, veremos que se trata de un layout para un traductor de idiomas. Nada especial,
salvo por el truco que nos permite ahorrarnos varios LinearLayouts en favor de un solo
RelativeLayout con una vista invisible que utilizamos para saber dónde está el centro con respecto a la altura de la pantalla. También nos encontramos con los Spinners, más conocidos como “ComboBox” o selector. Esencialmente, esconden una lista que pasa a primer plano cuando el usuario quiere elegir otro elemento, y son perfectos para casos como éste.
También veréis un TextWatcher unido al EditText del texto fuente en el código, pero
explicaremos su funcionamiento más adelante.
Lo realmente interesante viene ahora, cuando pulsemos las teclas Control + F12. Es
aquí cuando nuestro emulador de Android realiza una rotación, y vemos perfectamente que la
aplicación sigue funcionando, sin tener que cerrarse.
Entonces nos preguntamos, ¿cómo ha resuelto Android este problema? Sencillo. Ha
seguido nuestras instrucciones al pie de la letra: las vistas ocupan toda la anchura, y respetan utilizar la mitad superior de la pantalla para el texto de entrada y la mitad superior para el de salida.
Una vez visto esto, la pregunta sería: ¿hace falta todo un ejercicio de nivel intermedio
para esta tarea? La verdad es que no. En nuestro código inicial no hay ningún concepto nuevo,
es simplemente el layout, los Strings y la Actividad, que no tiene ningún elemento
novedoso. ¿Y entonces? ¿Para qué estamos aquí?
B---1. Interfaces específicas
Hay una gran infinidad de cosas que no podremos dar en este pequeño curso, y en este apartado utilizaremos un concepto que no tendremos tiempo de explicar al completo, pero que al menos os resultará familiar llegado el caso.
En fin, ¿qué es eso de Interfaces específicas? Veréis, tal y como tenemos la aplicación,
la interfaz es perfecta para el caso en el que la pantalla esté de pie, o portrait. Es cómoda e
intuitiva para este caso. Sin embargo cuando giramos el dispositivo, es horrible, porque los cuadros para introducir texto, que son lo más importante, tienen muy poco espacio, y resulta muy incómodo de utilizar. ¿Por qué? Porque esta interfaz ha sido diseñada para una pantalla alta, no ancha; estamos malgastando la gran anchura que tenemos, y lo estamos pagando en el poco espacio que hay para introducir texto. ¿Cómo lo solucionamos? Muy sencillo, haciendo
otro layout.
En la carpeta res guardamos los layouts, en una carpeta con el mismo nombre. La
carpeta layout es la carpeta a la que accede el sistema operativo para cargar la aplicación por defecto, cuando no encuentra nada mejor. Como no hay nada específico para ningún caso, siempre escoge el mismo layout. Cambiemos eso.
Lo primero que debemos hacer es crear una nueva carpeta dentro de res, que
llamaremos “layout-land”. Esta carpeta tiene un nombre pre---definido, y es la carpeta a la
que irá Android cuando tenga que cargar un layout en modo landscape. Dentro de esta carpeta copiaremos el fichero main.xml que ya tenemos de la carpeta layout, y ejecutamos la aplicación. ¿No se aprecia ningún cambio, verdad? Eso es porque el layout
específico para landscape es exactamente igual que el de portrait. Arreglémoslo.
Como en landscape tenemos mucha anchura, lo ideal sería, en vez de dividir la pantalla en dos por altura, hacerlo por anchura. Iremos vista a vista realizando los cambios pertinentes. Empezando por
screenCenter,, debemos cambiar android:layout_centerVertical por android:layout_centerHorizontal.
android:layout_toLeftOf=”@id/screenCenter”.
En el primer EditText debemos quitarle la sentencia android:layout_above y añadirle la misma que añadimos al Spinner, diciéndole que debe situarse a la izquierda de
screenCenter.
A continuación nos quedan el segundo Spinner y el segundo EditText. Al
Spinner debemos decirle primero que se sitúe en la parte superior de la pantalla, luego debemos añadirle
android:layout_alignParentTop=”true”quitarle android:layout_below y añadirle android:layout_toRightOf=”@id/screenCenter”
para que se sitúe a la derecha. De esta forma, el Spinner se colocará en la parte superior y se quedará a la derecha de nuestro marcador. Por último ya el EditText, al que le debemos quitar la
sentencia android:layout_above y añadirle android:layout_toRightOf,
, como
al segundo Spinner. Tras estos cambios, el resultado será el siguiente:
B---2. La Rotación no es perfecta
Ahora nos toca andar con un poquito más de ojo. Y es que, aunque parece que todo va bien, no es así. Revisando el código de initConfig(), y de la Actividad en general, encontramos la variable x. Es un String que, ayudándose de un TextWatcher que hemos asociado con el EditText que recoge el texto fuente, se actualiza con el contenido de éste
último. ¿Por qué? La respuesta la encontraremos al arrancar la aplicación, y al ver el LogCat
detrás. Si empezamos a escribir en el primer EditText, veremos en el LogCat que el valor de la variable x se está actualizando constantemente. ¿Perfecto, no? El TextWatcher es, sin duda, una herramienta muy útil, pero no debemos ser muy agresivos con ella. Cada vez que se
produce algún cambio en el EditText, se llaman a los tres métodos de todos los TextWatcher que tenga asociado el EditText (no es un listener, es un watcher, y podemos ponerle muchos) Si estos métodos no son rápidos, el usuario se dará cuenta al
escribir muchos caracteres juntos, ya que el retraso se irá acumulando hasta que la aplicación
obligue al usuario a parar de escribir.
Siguiendo ahora con nuestro ejercicio, rotemos el emulador. ¿Qué ocurre? Nada, aparentemente; el contenido del EditText se ha guardado, ¿no? Veamos el Log. ¿qué vemos? ¿hay algo extraño? Sí, Los métodos onCreate() e initConfig() han vuelto a ejecutarse, y el valor de x se perdió y volvió a recuperarse automáticamente. ¿Qué ha pasado?
Vayamos por partes. Para empezar, cuando un dispositivo Android rota, lo que ocurre es que la Actividad se destruye y vuelve a crearse; por eso se llaman a los métodos onCreate() e initConfig(); porque es como si acabáramos de arrancar la aplicación.
¿Y qué pasa con la variable x? Como toda la Actividad es destruida, x se pierde, pero se
recupera porque las vistas están preparadas para guardar su contenido de forma automática. Cuando la vista recupera su contenido (lo cambia), se ejecuta el watcher que actualiza a la variable x. Sin embargo, esto no siempre funciona como deseamos, y depende de la situación.
La norma es considerar que las vistas no guardan información, y que debemos hacerlo
nosotros mismos. ¿Cómo lo vamos a hacer? Lo primero es desquitarnos del TextWatcher,
ya que no nos hace falta en este caso; sólo queremos guardar en x el texto que hay en el EditText cuando éste vaya a rotar, porque mientras el usuario utiliza la aplicación no hay peligro, ¿no? Lo siguiente es, efectivamente, buscar la forma de guardar el contenido del
EditText en x justo antes de que se pierda y, conseguir recuperarlo cuando se reconstruya
la Actividad.
Para ello, lo primero que haremos es comentar todo el código del watcher, e impedir que sea añadido a nuestro EditText de texto fuente. Ahora mismo, la variable x no se actualizaría al escribir, pero esto no es un problema. Lo que nos hace falta para solucionar este problema es acudir al método onSaveInstanceState() de la Actividad. Este método se ejecuta cuando la Actividad pase a estar en estado detenida (onStop()), y se diferencia de la llamada anterior en que se nos pasa un objeto de tipo Bundle. Un Bundle es una clase que contiene información, y es utilizada por el sistema operativo para guardar el estado de la aplicación. ¿Qué queremos decir con estado? Muy sencillo. Supongamos que estamos realizando algo importante en una aplicación, y de repente recibimos una llamada de teléfono.
En ese momento, la aplicación desaparecerá y se quedará en memoria, esperando. Al terminar la llamada, podemos volver a la aplicación y todo estará allí. Sin embargo, esto ocurre suponiendo que el sistema operativo no ha necesitado eliminar nuestra aplicación de la
memoria por falta de ésta; si lo hubiera tenido que hacer, los datos se habrían perdido, a menos que hayamos guardado los datos de usuario en el método onPause(). Al volver, la
aplicación estaría como nueva, como recién arrancada, porque los datos se habrían guardado
en el onPause().
Y entonces, ¿para qué sirve el onSaveInstanceState() y su bundle? Para que el sistema operativo nos lo dé, y recuperemos el estado de la aplicación aunque nos haya borrado de la memoria, permitiéndonos actuar como si la aplicación nunca se hubiese cerrado,
cosa que seguro les gustará a nuestros usuarios. Éste es el código a añadir en la Actividad, bajo
el initConfig():
public void onSaveInstanceState(Bundle savedInstanceState) {
x = sourceText.getText().toString();
savedInstanceState.putString("x", x);
}
Probemos ahora. ¿Qué ocurre? ¿Por qué no se guarda el contenido del EditText
como antes?
Al sobre---escribir el método onSaveInstanceState() le hemos dicho a Android
que la responsabilidad de guardar el estado de la aplicación es nuestra, cosa que será
necesaria cuando tengamos que guardar información sobre el estado de la Actividad, como pueden ser instancias (variables) de clases Java. Esto significa que las vistas ya no intentan guardar sus datos y recuperarlos automáticamente; ahora tenemos que hacerlo nosotros.
Para finalizar con el ejercicio, y dejarlo tal y como estaba antes, necesitamos modificar un par de líneas dentro de initConfig(). Justo antes de inicializar x, debemos ejecutar un
if. Si la variable savedInstanceState no es null, debemos inicializar x recuperando su
valor del Bundle savedInstanceState con el método getString(), pasándole
“x” como argumento y asignar el texto del EditText sourceText a x. Si
savedInstanceState es null, inicializamos x normalmente.
¿Qué significa el argumento de tipo String que estamos utilizando? Dado que podemos guardar muchísimas cosas en el bundle, incluso del mismo tipo, necesitamos identificar cada una. Para hacerlo sencillo, hemos utilizado como identificador o llave (key) el string “x”. Además de Strings, podemos guardar todos los tipos básicos como ints, booleans, bytes, floats, doubles, etc. Pero aún más importante es el hecho de que podemos guardar clases nuestras, clases Java, si éstas implementan la interfaz Serializable. Debido a que esto último se escapa de los contenidos de nuestro curso, no
podemos explicarlo en detalle, pero animamos a los alumnos a investigarlo.
C. Conclusión
La rotación es un aspecto esencial de cualquier aplicación Android, y puede volverse más complejo de lo que pueda parecer, hasta el punto de que hay aplicaciones que se distribuyen sin permitir que el usuario rote la pantalla, quedándose siempre en la misma
posición, para así evitar tener que buscar una forma rápida e indolora que resuelva el
problema.
D. Opcionales
Como siempre, dejamos algunas cosas para que las hagáis e investiguéis vosotros por
vuestra cuenta:
--- Existe un atributo que se le puede dar a una Actividad desde el manifest, y que permite bloquear una Actividad para que se quede fija en una posición, o portrait o landscape. Investigarlo y probarlo.
--- Guardar también el contenido de los dos Spinners y del texto destino en el bundle,
y restaurarlos correctamente