Beruflich Dokumente
Kultur Dokumente
1.
2.
3.
1.
2.
3.
4.
1.
2.
3.
4.
5.
1.
2.
1.
2.
1.
1.
2.
3.
4.
5.
6.
1.
2.
3.
4.
1.
las opciones seleccionadas por defecto. Durante el proceso se instalar el SDK de Android,
los componentes adicionales para el desarrollo sobre Android 5.0, un dispositivo virtual (o
AVD, ms adelante veremos lo que es esto) preconfigurado para dicha versin de
Android, y por supuesto el entorno de desarrollo Android Studio.
Durante la instalacin tendremos que indicar tambin las rutas donde queremos instalar
tanto Android Studio como el SDK de Android. Para evitar posibles problemas futuros mi
recomendacin personal es seleccionar rutas que no contengan espacios en blanco.
Los componentes principales que, como mnimo, deberemos instalar/actualizar sern los
siguientes:
1.
2.
3.
4.
5.
10
11
El asistente de creacin del proyecto nos guiar por las distintas opciones de creacin y
configuracin de un nuevo proyecto Android.
En la primera pantalla indicaremos, por este orden, el nombre de la aplicacin, el dominio
de la compaa, y la ruta donde crear el projecto. El segundo de los datos indicados tan slo
se utilizar como paquete de nuestras clases java. As, si por ejemplo indicamos como en
mi caso android.sgoliver.net, el paquete java principal utilizado para mis clases
ser net.sgoliver.android.holausuario. En tu caso puedes utilizar cualquier otro dominio.
La versin mnima que seleccionemos en esta pantalla implicar que nuestra aplicacin se
pueda ejecutar en ms o menos dispositivos. De esta forma, cuanto menor sea sta, a ms
dispositivos podr llegar nuestra aplicacin, pero ms complicado ser conseguir que se
ejecute correctamente en todas las versiones de Android. Para hacernos una idea del
nmero de dispositivos que cubrimos con cada versin podemos pulsar sobre el enlace
Help me choose, que mostrar el porcentaje de dispositivos que ejecutan actualmente
cada versin de Android. Por ejemplo, en el momento de escribir este artculo, si
12
Por ltimo, en el siguiente paso del asistente indicaremos los datos asociados a esta
actividad principal que acabamos de elegir, indicando el nombre de su clase java asociada
(Activity Name) y el nombre de su layout xml (algo as como la interfaz grfica de la
actividad, lo veremos ms adelante), su ttulo, y el nombre del recurso XML
correspondiente a su men principal. No nos preocuparemos mucho por ahora de todos
estos datos por lo que podemos dejar todos los valores por defecto. Ms adelante en el
curso explicaremos cmo y para qu utilizar estos elementos.
13
Una vez configurado todo pulsamos el botn Finish y Android Studio crear por nosotros
toda la estructura del proyecto y los elementos indispensables que debe contener. Si todo va
bien aparecer la pantalla principal de Android Studio con el nuevo proyecto creado.
En la parte izquierda, podemos observar todos los elementos creados inicialmente para
el nuevo proyecto Android, sin embargo por defecto los vemos de una forma un tanto
peculiar que podra llevarnos a confusin. Para entender mejor la estructura del proyecto
vamos a cambiar momentneamente la forma en la que Android Studio nos la muestra. Para
ello, pulsaremos sobre la lista desplegable situada en la parte superior izquierda, y
cambiaremos la vista de proyecto a Project.
14
Tras hacer esto, la estructura del proyecto cambia un poco de aspecto y pasa a ser como se
observa en la siguiente imagen:
15
ejemplo en este caso que estamos creando tenemos el proyecto android-hola-usuario que
contiene al mdulo app que contendr todo el software de la aplicacin de ejemplo.
Carpeta /app/src/main/res/
Contiene todos los ficheros de recursos necesarios para el proyecto: imgenes, layouts,
cadenas de texto, etc. Los diferentes tipos de recursos se pueden distribuir entre las
siguientes subcarpetas:
Carpeta
Descripcin
/res/drawable/
16
/res/layout/
/res/anim/
/res/animator/
/res/color/
/res/menu/
/res/xml/
/res/raw/
/res/values/
colores
No todas estas carpetas tienen por qu aparecer en cada proyecto Android, tan slo las que
se necesiten. Iremos viendo durante el curso qu tipo de elementos se pueden incluir en
cada una de ellas y cmo se utilizan.
Como ejemplo, para un proyecto nuevo Android como el que hemos creado, tendremos por
defecto los siguientes recursos para la aplicacin:
17
Como se puede observar, existen algunas carpetas en cuyo nombre se incluye un sufijo
adicional, como por ejemplo values-w820dp. Estos, y otros sufijos, se emplean para
definir recursos independientes para determinados dispositivos segn sus caractersticas. De
esta forma, por ejemplo, los recursos incluidos en la carpeta values-w820dp se aplicaran
slo a pantallas con ms de 820dp de ancho, o los incluidos en una carpeta
llamada values-v11 se aplicaran tan slo a dispositivos cuya versin de Android sea la
3.0 (API 11) o superior. Al igual que los sufijos -w y v existen otros muchos para
referirse a otras caractersticas del terminal, puede consultarse la lista completa en
la documentacin oficial del Android.
Entre los recursos creados por defecto cabe destacar los layouts, en nuestro caso slo
tendremos por ahora el llamado activity_main.xml, que contienen la definicin de la
interfaz grfica de la pantalla principal de la aplicacin. Si hacemos doble clic sobre este
fichero Android Studio nos mostrar esta interfaz en su editor grfico, y como podremos
comprobar, en principio contiene tan slo una etiqueta de texto con el mensaje Hello
World!.
18
Pulsando sobre las pestaas inferiores Design y Text podremos alternar entre el editor
grfico (tipo arrastrar-y-soltar), mostrado en la imagen anterior, y el editor XML que se
muestra en la imagen siguiente:
19
A destacar sobre todo el fichero que aparece desplegado en la imagen anterior, llamado
R.java, donde se define la clase R. Esta clase R contendr en todo momento una serie de
constantes con los identificadores (ID) de todos los recursos de la aplicacin incluidos en la
carpeta /app/src/main/res/, de forma que podamos acceder fcilmente a estos recursos desde
nuestro
cdigo
a
travs
de
dicho dato.
As,
por
ejemplo,
la
constante R.layout.activity_main contendr el ID del layout activity_main.xml contenido
en la carpeta /app/src/main/res/layout/.
20
Y con esto todos los elementos principales de un proyecto Android. No pierdas de vista este
proyecto de ejemplo que hemos creado ya que lo utilizaremos en breve como base para
crear nuestra primera aplicacin. Pero antes, en el siguiente apartado hablaremos de los
componentes software principales con los que podemos construir una aplicacin Android.
21
Service
Los servicios (service) son componentes sin interfaz grfica que se ejecutan en segundo
plano. En concepto, son similares a los servicios presentes en cualquier otro sistema
operativo. Los servicios pueden realizar cualquier tipo de acciones, por ejemplo actualizar
datos, lanzar notificaciones, o incluso mostrar elementos visuales (p.ej. actividades) si se
necesita en algn momento la interaccin con del usuario.
Content Provider
Un proveedor de contenidos (content provider) es el mecanismo que se ha definido en
Android para compartir datos entre aplicaciones. Mediante estos componentes es posible
compartir determinados datos de nuestra aplicacin sin mostrar detalles sobre su
almacenamiento interno, su estructura, o su implementacin. De la misma forma, nuestra
aplicacin podr acceder a los datos de otra a travs de loscontent provider que se hayan
definido.
Broadcast Receiver
Un broadcast receiver es un componente destinado a detectar y reaccionar ante
determinados mensajes o eventos globales generados por el sistema (por ejemplo: Batera
baja, SMS recibido, Tarjeta SD insertada, ) o por otras aplicaciones (cualquier
aplicacin puede generar mensajes (intents, en terminologa Android) broadcast, es decir,
no dirigidos a una aplicacin concreta sino a cualquiera que quiera escucharlo).
Widget
Los widgets son elementos visuales, normalmente interactivos, que pueden mostrarse en la
pantalla principal (home screen) del dispositivo Android y recibir actualizaciones
peridicas. Permiten mostrar informacin de la aplicacin al usuario directamente sobre la
pantalla principal.
Intent
Un intent es el elemento bsico de comunicacin entre los distintos componentes Android
que hemos descrito anteriormente. Se pueden entender como los mensajes o peticiones que
son enviados entre los distintos componentes de una aplicacin o entre distintas
aplicaciones. Mediante un intent se puede mostrar una actividad desde cualquier otra,
iniciar un servicio, enviar un mensaje broadcast, iniciar otra aplicacin, etc.
En el siguiente artculo empezaremos ya a ver algo de cdigo, analizando al detalle una
aplicacin sencilla.
22
Vamos a partir del proyecto de ejemplo que creamos en un apartado anterior, al que
casualmente llamamos HolaUsuario.
Como ya vimos Android Studio haba creado por nosotros la estructura de carpetas del
proyecto y todos los ficheros necesarios de un Hola Mundo bsico, es decir, una sola
pantalla donde se muestra nicamente un mensaje fijo.
Lo primero que vamos a hacer es disear nuestra pantalla principal modificando la que
Android Studio nos ha creado por defecto. Aunque ya lo hemos comentado de pasada,
recordemos dnde y cmo se define cada pantalla de la aplicacin. En Android, el diseo y
la lgica de una pantalla estn separados en dos ficheros distintos. Por un lado, en el fichero
/src/main/res/layout/activity_main.xml tendremos el diseo puramente visual de la pantalla
definido
como
fichero
XML
por
otro
lado,
en
el
fichero
23
24
Dentro del layout hemos incluido 3 controles: una etiqueta (TextView), un cuadro de texto
(EditText), y un botn (Button). En todos ellos hemos establecido las siguientes
propiedades:
android:id. ID del control, con el que podremos identificarlo ms tarde en nuestro cdigo.
Vemos que el identificador lo escribimos precedido de @+id/. Esto tendr como efecto
que al compilarse el proyecto se genere automticamente una nueva constante en la
clase R para dicho control. As, por ejemplo, como al cuadro de texto le hemos asignado el
ID TxtNombre, podremos ms tarde acceder al l desde nuestro cdigo haciendo referencia
a la constante R.id.TxtNombre.
android:layout_height y android:layout_width. Dimensiones del control con respecto al
layout que lo contiene (height=alto, width=ancho). Esta propiedad tomar normalmente los
valores wrap_content para indicar que las dimensiones del control se ajustarn al
contenido del mismo, o bien match_parent para indicar que el ancho o el alto del control
se ajustar al alto o ancho del layout contenedor respectivamente.
Adems de estas propiedades comunes a casi todos los controles que utilizaremos, en el
cuadro de texto hemos establecido tambin la propiedad android:inputType, que indica qu
tipo de contenido va a albergar el control, en este caso ser texto normal (valor text),
aunque podra haber sido una contrasea (valor textPassword), un telfono (phone),
una fecha (date), .
Por ltimo, en la etiqueta y el botn hemos establecido la propiedad android:text, que
indica el texto que aparece en el control. Y aqu nos vamos a detener un poco, ya que
tenemos dos alternativas a la hora de hacer esto. En Android, el texto de un control se
puede especificar directamente como valor de la propiedad android:text, o bien utilizar
alguna de las cadenas de texto definidas en los recursos del proyecto (como ya vimos, en el
fichero strings.xml), en cuyo caso indicaremos como valor de la propiedad android:text su
identificador precedido del prefijo @string/. Dicho de otra forma, la primera alternativa
habra sido indicar directamente el texto como valor de la propiedad, por ejemplo en la
etiqueta de esta forma:
1 <TextView
2
android:id="@+id/LblNombre"
3
android:layout_width="wrap_content"
4
android:layout_height="wrap_content"
5
android:text="Escribe tu nombre:" />
Y la segunda alternativa, la utilizada en el ejemplo, consistira en definir primero una nueva
cadena de texto en el fichero de recursos /src/main/res/values/strings.xml, por ejemplo con
identificador nombre y valor Escribe tu nombre:.
1 <resources>
2
...
3
<string name="nombre">Escribe tu nombre:</string>
4
...
5 </resources>
25
26
26
// automatically handle clicks on the Home/Up button, so long
27
// as you specify a parent activity in AndroidManifest.xml.
28
int id = item.getItemId();
29
30
//noinspection SimplifiableIfStatement
31
if (id == R.id.action_settings) {
32
return true;
33
}
34
35
return super.onOptionsItemSelected(item);
36
}
37 }
Como ya vimos en un apartado anterior, las diferentes pantallas de una aplicacin Android
se definen mediante objetos de tipo Activity. Por tanto, lo primero que encontramos en
nuestro fichero java es la definicin de una nueva clase MainActivity que extiende en este
caso de un tipo especial de Activityllamado ActionBarActivity, que soporta la utilizacin
de la Action Bar en nuestras aplicaciones (la action bar es la barra de ttulo y men superior
que se utiliza en la mayora de aplicaciones Android). El nico mtodo que modificaremos
por ahora de esta clase ser el mtodo onCreate(), llamado cuando se crea por primera vez
la actividad. En este mtodo lo nico que encontramos en principio, adems de la llamada a
su
implementacin
en
la
clase
padre,
es
la
llamada
al
mtodo setContentView(R.layout.activity_main). Con esta llamada estaremos indicando a
Android que debe establecer como interfaz grfica de esta actividad la definida en el
recurso R.layout.activity_main, que no es ms que la que hemos especificado en el
fichero/src/main/res/layout/activity_main.xml. Una vez ms vemos la utilidad de las
diferentes constantes de recursos creadas automticamente en la clase R al compilar el
proyecto.
Adems del mtodo onCreate(), vemos que tambin se sobrescriben los
mtodos onCreateOptionsMenu() y onOptionsItemSelected(), que se utilizan para definir y
gestionar los mens de la aplicacin y/o las opciones de la action bar. Por el momento no
tocaremos estos mtodos, ms adelante en el curso nos ocuparemos de estos temas.
Antes de modificar el cdigo de nuestra actividad principal, vamos a crear una nueva
actividad para la segunda pantalla de la aplicacin anloga a sta primera, a la que
llamaremos SaludoActivity.
Para ello, pulsaremos el botn derecho sobre la carpeta /src/main/java/tu.paquete.java/ y
seleccionaremos la opcin de men New / Activity / Blank Activity.
27
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/LytContenedorSaludo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
28
7
<TextView android:id="@+id/TxtSaludo"
8
android:layout_width="wrap_content"
9
android:layout_height="wrap_content"
10
android:text="" />
11
12 </LinearLayout>
Por su parte, si revisamos ahora el cdigo de la clase java SaludoActivity veremos que es
anlogo a la actividad principal:
1
public class SaludoActivity extends ActionBarActivity {
2
3
@Override
4
protected void onCreate(Bundle savedInstanceState) {
5
super.onCreate(savedInstanceState);
6
setContentView(R.layout.activity_saludo);
7
}
8
9
//...
10 }
Sigamos. Por ahora, el cdigo incluido en estas clases lo nico que hace es generar la
interfaz de la actividad. A partir de aqu nosotros tendremos que incluir el resto de la lgica
de la aplicacin.
Y vamos a empezar con la actividad principal MainActivity, obteniendo una referencia a
los diferentes controles de la interfaz que necesitemos manipular, en nuestro caso slo el
cuadro de texto y el botn. Para ello definiremos ambas referencias como atributos de la
clase y para obtenerlas utilizaremos el mtodo findViewById() indicando el ID de cada
control, definidos como siempre en la clase R. Todo esto lo haremos dentro del
mtodo onCreate() de la clase MainActivity, justo a continuacin de la llamada
a setContentView() que ya comentamos.
1
2
3
4
5
6
7
8
9
10
11
package net.sgoliver.android.holausuario;
//..
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends ActionBarActivity {
private EditText txtNombre;
private Button btnAceptar;
29
12
@Override
13
protected void onCreate(Bundle savedInstanceState) {
14
super.onCreate(savedInstanceState);
15
setContentView(R.layout.activity_main);
16
17
//Obtenemos una referencia a los controles de la interfaz
18
txtNombre = (EditText)findViewById(R.id.TxtNombre);
19
btnAceptar = (Button)findViewById(R.id.BtnAceptar);
20
}
21
22
//...
23 }
Como vemos, hemos aadido tambin varios import adicionales (los de las
clases Button y EditText) para tener acceso a todas las clases utilizadas.
Una vez tenemos acceso a los diferentes controles, ya slo nos queda implementar las
acciones a tomar cuando pulsemos el botn de la pantalla. Para ello, continuando el cdigo
anterior, y siempre dentro del mtodo onCreate(), implementaremos el evento onClick de
dicho botn. Este botn tendr que ocuparse de abrir la actividad SaludoActivity pasndole
toda la informacin necesaria. Veamos cmo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package net.sgoliver.android.holausuario;
//...
import android.content.Intent;
public class MainActivity extends ActionBarActivity {
private EditText txtNombre;
private Button btnAceptar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Obtenemos una referencia a los controles de la interfaz
txtNombre = (EditText)findViewById(R.id.TxtNombre);
btnAceptar = (Button)findViewById(R.id.BtnAceptar);
//Implementamos el evento click del botn
btnAceptar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
30
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//Creamos el Intent
Intent intent =
new Intent(MainActivity.this, SaludoActivity.class);
//Creamos la informacin a pasar entre actividades
Bundle b = new Bundle();
b.putString("NOMBRE", txtNombre.getText().toString());
//Aadimos la informacin al intent
intent.putExtras(b);
//Iniciamos la nueva actividad
startActivity(intent);
}
});
}
}
31
Con esto hemos finalizado ya actividad principal de la aplicacin, por lo que pasaremos ya
a la secundaria. Comenzaremos de forma anloga a la anterior, ampliando el
mtodo onCreate() obteniendo las referencias a los objetos que manipularemos, esta vez
slo la etiqueta de texto. Tras esto viene lo ms interesante, debemos recuperar la
informacin pasada desde la actividad principal y asignarla como texto de la etiqueta. Para
ello accederemos en primer lugar al intent que ha originado la actividad actual mediante el
mtodo getIntent() y recuperaremos su informacin asociada (objeto Bundle) mediante el
mtodo getExtras().
Hecho esto tan slo nos queda construir el texto de la etiqueta mediante su
mtodo setText(texto) y recuperando el valor de nuestra clave almacenada en el
objeto Bundle mediante getString(clave).
1
package net.sgoliver.android.holausuario;
2
3
//...
4
import android.widget.TextView;
5
6
public class SaludoActivity extends ActionBarActivity {
7
8
private TextView txtSaludo;
9
10
@Override
11
protected void onCreate(Bundle savedInstanceState) {
12
super.onCreate(savedInstanceState);
13
setContentView(R.layout.activity_saludo);
14
15
//Localizar los controles
16
txtSaludo = (TextView)findViewById(R.id.TxtSaludo);
17
18
//Recuperamos la informacin pasada en el intent
19
Bundle bundle = this.getIntent().getExtras();
20
21
//Construimos el mensaje a mostrar
22
txtSaludo.setText("Hola " + bundle.getString("NOMBRE"));
23
}
24
25
//...
26 }
Con esto hemos concluido la lgica de las dos pantallas de nuestra aplicacin y tan slo nos
queda un paso importante para finalizar nuestro desarrollo. Como ya indicamos en un
apartado anterior, toda aplicacin Android utiliza un fichero especial en formato XML
(AndroidManifest.xml) para definir, entre otras cosas, los diferentes elementos que la
32
componen. Por tanto, todas las actividades de nuestra aplicacin deben quedar
convenientemente definidas en este fichero. En este caso, Android Studio se debe haber
ocupado por nosotros de definir ambas actividades en el fichero, pero lo revisaremos para
as echar un vistazo al contenido.
Si abrimos el fichero AndroidManifest.xml veremos que contiene un elemento
principal <Application> que debe incluir varios elementos <Activity>, uno por cada
actividad incluida en nuestra aplicacin. En este caso, comprobamos como efectivamente
Android Studio ya se ha ocupado de esto por nosotros, aunque este fichero s podramos
modificarlo a mano para hacer ajustes si fuera necesario.
1
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
package="net.sgoliver.android.holausuario" >
3
4
<application
5
android:allowBackup="true"
6
android:icon="@drawable/ic_launcher"
7
android:label="@string/app_name"
8
android:theme="@style/AppTheme" >
9
<activity
10
android:name=".MainActivity"
11
android:label="@string/app_name" >
12
<intent-filter>
13
<action android:name="android.intent.action.MAIN" />
14
<category android:name="android.intent.category.LAUNCHER" />
15
</intent-filter>
16
</activity>
17
<activity
18
android:name=".SaludoActivity"
19
android:label="@string/title_activity_saludo" >
20
</activity>
21
</application>
22 </manifest>
Podemos ver como para cada actividad se indica entre otras cosas el nombre de su clase
java asociada como valor del atributo android:name, y su ttulo mediante el
atributo android:label, ms adelante veremos qu opciones adicionales podemos
especificar. Vemos una vez ms cmo el ttulo de las actividades se indica como referencia
a cadenas de caracteres definidas como recursos, que deben estar incluidas como ya hemos
comentado anteriormente en el fichero /main/res/values/strings.xml
El ltimo elemento que revisaremos de nuestro proyecto, aunque tampoco tendremos que
modificarlo por ahora, ser el fichero build.gradle. Pueden existir varios ficheros llamados
as en nuestra estructura de carpetas, a distintos niveles, pero normalmente siempre
33
accederemos al que est al nivel ms interno, en nuestro caso el que est dentro del mdulo
app. Veamos qu contiene:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
34
Pulsando el botn central Create a virtual device accederemos al asistente para crear un
AVD. En el primer paso tendremos que seleccionar a la izquierda qu tipo de dispositivo
queremos que simule nuestro AVD (telfono, tablet, reloj, ) y el tamao, resolucin, y
densidad de pxeles de su pantalla. En mi caso seleccionar por ejemplo las caractersticas
de un Nexus 4 y pasaremos al siguiente paso pulsando Next.
35
Tras pulsar el botn Finish tendremos ya configurado nuestro AVD, por lo que podremos
comenzar a probar nuestras aplicaciones sobre l.
Para ello pulsaremos simplemente el men Run / Run app (o la tecla rpida Mays+F10).
Android Studio nos preguntar en qu dispositivo queremos ejecutar la aplicacin y nos
mostrar dos listas. La primera de ellas con los dispositivos que haya en ese momento en
funcionamiento (por ejemplo si ya tenamos un emulador funcionando) y una lista
36
Si todo va bien, tras una pequea (o no tan pequea) espera aparecer el emulador de
Android y se iniciar automticamente nuestra aplicacin (si se inicia el emulador pero no
se ejecuta automticamente la aplicacin podemos volver a ejecutarla desde Android
Studio, mediante el men Run, sin cerrar el emulador ya abierto).
Podemos probar a escribir un nombre y pulsar el botn Aceptar para comprobar si el
funcionamiento es el correcto.
37
Y con esto terminamos por ahora. Espero que esta aplicacin de ejemplo os sea de ayuda
para aprender temas bsicos en el desarrollo para Android, como por ejemplo la definicin
de la interfaz grfica, el cdigo java necesario para acceder y manipular los elementos de
dicha interfaz, y la forma de comunicar diferentes actividades de Android. En los apartados
siguientes veremos algunos de estos temas de forma mucho ms especfica.
Podis consultar online y descargar el cdigo fuente completo de este artculo desde github.
38
FrameLayout
ste es el ms simple de todos los layouts de Android. Un FrameLayout coloca todos sus
controles hijos alineados con su esquina superior izquierda, de forma que cada control
quedar oculto por el control siguiente (a menos que ste ltimo tenga transparencia). Por
ello, suele utilizarse para mostrar un nico control en su interior, a modo de contenedor
(placeholder) sencillo para un slo elemento sustituible, por ejemplo una imagen.
Los componentes incluidos en un FrameLayout podrn establecer sus propiedades
android:layout_width y android:layout_height,
que podrn tomar
los
valores
match_parent (para que el control hijo tome la dimensin de su layout contenedor) o
wrap_content (para que el control hijo tome la dimensin de su contenido). Veamos un
ejemplo:
Ejemplo:
1
2
3
4
5
6
7
8
9
10
11
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText android:id="@+id/TxtNombre"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="text" />
</FrameLayout>
LinearLayout
El siguiente tipo de layout en cuanto a nivel de complejidad es el LinearLayout. Este layout
apila uno tras otro todos sus elementos hijos en sentido horizontal o vertical segn se
establezca su propiedad android:orientation.
39
40
18
19
</LinearLayout>
Con el cdigo anterior conseguiramos un layout como el siguiente:
As pues, a pesar de la simplicidad aparente de este layout resulta ser lo suficiente verstil
como para sernos de utilidad en muchas ocasiones.
TableLayout
Un TableLayout permite distribuir sus elementos hijos de forma tabular, definiendo las filas
y columnas necesarias, y la posicin de cada componente dentro de la tabla.
La estructura de la tabla se define de forma similar a como se hace en HTML, es decir,
indicando las filas que compondrn la tabla (objetos TableRow), y dentro de cada fila las
columnas necesarias, con la salvedad de que no existe ningn objeto especial para definir
una columna (algo as como unTableColumn) sino que directamente insertaremos los
controles necesarios dentro del TableRow y cada componente insertado (que puede ser un
control sencillo o incluso otro ViewGroup) corresponder a una columna de la tabla. De
esta forma, el nmero final de filas de la tabla se corresponder con el nmero de
elementos TableRow insertados, y el nmero total de columnas quedar determinado por el
nmero de componentes de la fila que ms componentes contenga.
Por norma general, el ancho de cada columna se corresponder con el ancho del mayor
componente de dicha columna, pero existen una serie de propiedades que nos ayudarn a
modificar este comportamiento:
41
Todas estas propiedades del TableLayout pueden recibir una lista de ndices de columnas
separados por comas (ejemplo: android:stretchColumns=1,2,3) o un asterisco para indicar
que debe aplicar a todas las columnas (ejemplo: android:stretchColumns=*).
Otra caracterstica importante es la posibilidad de que una celda determinada pueda ocupar
el espacio de varias columnas de la tabla (anlogo al atributo colspan de HTML). Esto se
indicar mediante la propiedad android:layout_span del componente concreto que deber
tomar dicho espacio.
Veamos un ejemplo con varios de estos elementos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TableRow>
<TextView android:text="Celda 1.1" />
<TextView android:text="Celda 1.2" />
<TextView android:text="Celda 1.3" />
</TableRow>
<TableRow>
<TextView android:text="Celda 2.1" />
<TextView android:text="Celda 2.2" />
<TextView android:text="Celda 2.3" />
</TableRow>
<TableRow>
<TextView android:text="Celda 3.1"
android:layout_span="2" />
<TextView android:text="Celda 3.2" />
</TableRow>
</TableLayout>
42
GridLayout
Este tipo de layout fue incluido a partir de la API 14 (Android 4.0) y sus caractersticas son
similares alTableLayout, ya que se utiliza igualmente para distribuir los diferentes
elementos de la interfaz de forma tabular, distribuidos en filas y columnas. La diferencia
entre ellos estriba en la forma que tiene elGridLayout de colocar y distribuir sus elementos
hijos en el espacio disponible. En este caso, a diferencia del TableLayout indicaremos el
nmero
de
filas
y
columnas
como
propiedades
del
layout,
medianteandroid:rowCount y android:columnCount. Con estos datos ya no es necesario
ningn tipo de elemento para indicar las filas, como hacamos con el
elemento TableRow del TableLayout, sino que los diferentes elementos hijos se irn
colocando
ordenadamente
por
filas
o
columnas
(dependiendo
de
la
propiedad android:orientation) hasta completar el nmero de filas o columnas indicadas en
los atributos anteriores. Adicionalmente, igual que en el caso anterior, tambin tendremos
disponibles las propiedades android:layout_rowSpan y android:layout_columnSpan para
conseguir que una celda ocupe el lugar de varias filas o columnas.
Existe tambin una forma de indicar de forma explcita la fila y columna que debe ocupar
un determinado elemento hijo contenido en el GridLayout, y se consigue utilizando los
atributos android:layout_row yandroid:layout_column. De cualquier forma, salvo para
configuraciones complejas del grid no suele ser necesario utilizar estas propiedades.
Con todo esto en cuenta, para conseguir una distribucin equivalente a la del ejemplo
anterior delTableLayout, necesitaramos escribir un cdigo como el siguiente:
1
2
3
4
5
6
7
<GridLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:rowCount="2"
android:columnCount="3"
android:orientation="horizontal" >
43
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RelativeLayout
El ltimo tipo de layout que vamos a ver es el RelativeLayout. Este layout permite
especificar la posicin de cada elemento de forma relativa a su elemento padre o a
cualquier otro elemento incluido en el propio layout. De esta forma, al incluir un nuevo
elemento X podremos indicar por ejemplo que debe colocarsedebajo del elemento
Y y alineado a la derecha del layout padre. Veamos esto en el ejemplo siguiente:
1
<RelativeLayout
2
xmlns:android="http://schemas.android.com/apk/res/android"
3
android:layout_width="match_parent"
4
android:layout_height="match_parent" >
5
6
<EditText android:id="@+id/TxtNombre"
7
android:layout_width="match_parent"
8
android:layout_height="wrap_content"
9
android:inputType="text" />
10
11
<Button android:id="@+id/BtnAceptar"
12
android:layout_width="wrap_content"
13
android:layout_height="wrap_content"
14
android:layout_below="@id/TxtNombre"
15
android:layout_alignParentRight="true" />
16
</RelativeLayout>
En
el
ejemplo,
el
botn BtnAceptar se
colocar
debajo
del
cuadro
de
44
android:layout_above
android:layout_below
android:layout_toLeftOf
android:layout_toRightOf
android:layout_alignLeft
android:layout_alignRight
android:layout_alignTop
android:layout_alignBottom
android:layout_alignBaseline
android:layout_alignParentLeft
android:layout_alignParentRight
android:layout_alignParentTop
android:layout_alignParentBottom
android:layout_centerHorizontal
android:layout_centerVertical
android:layout_centerInParent
Por ltimo indicar que cualquiera de los tipos de layout anteriores poseen otras propiedades
comunes como por ejemplo los mrgenes exteriores (margin) e interiores (padding) que
pueden establecerse mediante los siguientes atributos:
android:layout_margin
android:layout_marginBottom
android:layout_marginTop
45
android:layout_marginLeft
android:layout_marginRight
android:padding
android:paddingBottom
android:paddingTop
android:paddingLeft
android:paddingRight
Existen otros layouts algo ms sofisticados a los que dedicaremos artculos especficos un
poco ms adelante, como por ejemplo el DrawerLayout para aadir mens laterales
deslizantes.
Tambin en prximos artculos veremos otros elementos comunes que extienden
a ViewGroup, como por ejemplo las vistas de tipo lista (ListView), de tipo grid
(GridView), y las pestaas o tabs (TabHost/TabWidget).
Button [API]
46
47
2
android:textOn="@string/on"
3
android:textOff="@string/off"
4
android:layout_width="wrap_content"
5
android:layout_height="wrap_content" />
Y su aspecto sera el siguiente:
48
Esto nos da acceso a Asset Studio, donde podremos indicar el tipo de imagen a aadir
(icono de lanzador, icono de action bar, icono de notificacin, ), el origen de la imagen
(Image = Fichero externo, Clipart = Coleccin de iconos estandar, Text = Texto
personalizado), el tema de nuestra aplicacin (lo que afectar al color de fondo y primer
plano de los iconos seleccionados), y el nombre del recurso a incluir en el proyecto.
As, en nuestro caso de ejemplo, seleccion Clipart como origen de la imagen, seleccion
el icono de estrella mediante el botn Choose, e indiqu el nombre ic_estrella:
49
Cabe decir adems, que aunque existe este tipo especfico de botn para imgenes, tambin
es posible aadir una imagen a un botn normal de tipo Button, a modo de elemento
suplementario al texto (compound drawable). Por ejemplo, si quisiramos aadir un icono a
la
izquierda
del
texto
de
un
botn
utilizaramos
la
propiedad android:drawableLeft indicando como valor el descriptor (ID) de la imagen que
queremos mostrar, y si fuera necesario podramos indicar tambin el espacio entre la
imagen y el texto mediante la propiedad android:drawablePadding:
1 <Button android:id="@+id/BtnBotonMasImagen"
2
android:text="@string/click"
3
android:drawableLeft="@drawable/ic_estrella"
4
android:drawablePadding="5dp"
5
android:layout_width="wrap_content"
6
android:layout_height="wrap_content" />
El botn mostrado en este caso sera similar a ste:
Eventos de un botn
Como podis imaginar, aunque estos controles pueden lanzar muchos otros eventos, el ms
comn de todos ellos y el que querremos capturar en la mayora de las ocasiones es el
evento onClick, que se lanza cada vez que el usuario pulsa el botn. Para definir la lgica
de
este
evento
tendremos
que
implementarla
definiendo
un
nuevo
objeto View.OnClickListener() y asocindolo al botn mediante el mtodo
setOnClickListener(). La forma ms habitual de hacer esto es la siguiente:
50
1
2
3
4
5
6
7
8
btnBotonSimple = (Button)findViewById(R.id.BtnBotonSimple);
btnBotonSimple.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0)
{
lblMensaje.setText("Botn Simple pulsado!");
}
});
btnToggle = (ToggleButton)findViewById(R.id.BtnToggle);
btnToggle.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0)
{
if(btnToggle.isChecked())
lblMensaje.setText("Botn Toggle: ON");
else
lblMensaje.setText("Botn Toggle: OFF");
}
});
51
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false" android:drawable="@drawable/toggle_off"
/>
<item android:state_checked="true" android:drawable="@drawable/toggle_on" />
</selector>
En el cdigo anterior vemos cmo se asigna a cada posible estado del botn una imagen (un
elemento drawable) determinada. As, por ejemplo, para el estado pulsado
(state_checked=true) se asigna la imagen toggle_on.
Este selector lo guardamos por ejemplo en un fichero llamado toggle_style.xml y lo
colocamos como un recurso ms en nuestra carpeta de recursos /res/drawable. Hecho esto,
tan slo bastara hacer referencia a este nuevo recurso que hemos creado en la
propiedad android:background del botn:
1
2
3
4
5
6
<ToggleButton android:id="@+id/BtnPersonalizado"
android:textOn="@string/on"
android:textOff="@string/off"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/toggle_style" />
En
la
siguiente
imagen
vemos
el
aspecto
nuestro ToggleButton personalizado con los cambios indicados:
por
defecto
de
52
pero prescindiendo sus bordes de forma que adquiera un aspecto plano y se intergre mejor
con
el
diseo
de
la
interfaz.
Para
ello,
podemos
utilizar
el
estilo borderlessButtonStyle como estilo del botn (propiedad style), de forma que ste se
mostrar sin bordes pero conservar otros detalles como el cambio de apariencia al ser
pulsado. Veamos cmo se definira por ejemplo un ImageButton sin borde:
1 <ImageButton android:id="@+id/BtnSinBorde"
2
android:layout_width="wrap_content"
3
android:layout_height="wrap_content"
4
android:contentDescription="@string/icono_ok"
5
android:src="@drawable/ic_estrella"
6
style="?android:borderlessButtonStyle"/>
En la siguiente imagen vemos cmo quedara este botn integrado dentro de
un LinearLayout y alineado a la derecha:
El separador vertical que se muestra entre el texto y el botn se consigue utilizando las
propiedadesshowDividers, divider, y dividerPadding del layout contenedor (para mayor
claridad puede consultarse el cdigo completo):
1 <LinearLayout
2
android:id="@+id/BarraBtnSinBorde"
3
android:layout_width="match_parent"
4
android:layout_height="wrap_content"
5
android:orientation="horizontal"
6
android:showDividers="middle"
7
android:divider="?android:dividerVertical"
8
android:dividerPadding="6dp" >
Otro lugar muy habitual donde encontrar botones sin borde es en las llamadas barras de
botones (button bar) que muestran muchas aplicaciones. Para definir una barra de botones,
utilizaremos normalmente como contenedor un LinearLayout horizontal e incluiremos
dentro de ste los botones (Button) necesarios, asignando a cada elelemento su estilo
correspondiente,
en
este
caso buttonBarStyle para
el
contenedor,
y buttonBarButtonStyle para los botones. En nuetro ejemplo crearemos una barra con dos
botones, Aceptar y Cancelar, que quedara as:
1
<LinearLayout
2
android:id="@+id/BarraBotones"
3
android:layout_width="match_parent"
4
android:layout_height="wrap_content"
5
android:orientation="horizontal"
6
android:layout_alignParentBottom="true"
7
style="?android:attr/buttonBarStyle">
8
53
9
<Button android:id="@+id/BtnAceptar"
10
android:layout_width="wrap_content"
11
android:layout_height="wrap_content"
12
android:layout_weight="1"
13
android:text="@string/Aceptar"
14
style="?android:attr/buttonBarButtonStyle" />
15
16
<Button android:id="@+id/BtnCancelar"
17
android:layout_width="wrap_content"
18
android:layout_height="wrap_content"
19
android:layout_weight="1"
20
android:text="@string/Cancelar"
21
style="?android:attr/buttonBarButtonStyle" />
22
23 </LinearLayout>
Visualmente el resultado sera el siguiente:
El inconveniente de este tipo de botones es que no estn incluidos de forma nativa en las
API de la plataforma, por lo que tenemos que construirlos como control
personalizado (ms adelante en el curso hablaremos de esto) o bien utilizar alguna de las
muchas libreras externas que ya lo implementan. Para no complicar ms este captulo voy
a indicar simplemente cmo utilizar una de las libreras open source ms sencillas que
implementan este tipo de control, disponible como proyecto en github: android-floatingaction-button.
Aadir libreras externas a un proyecto de Android Studio
Para hacer uso de una librera externa en un proyecto de Android Studio tenemos dos
posibilidades: aadir el fichero jar de la librera a la carpeta /libs del mdulo, o bien aadir
la referencia a la librera (si est disponible) como dependencia en el
fichero build.gradle del mdulo. En este caso, en la web de la librera nos informan de los
datos necesarios para aadir la librera como dependencia (apartado Usage de la web).
54
Por tanto usaremos esta opcin aadiendo a nuestro ficherobuild.gradle la siguiente lnea
en el apartado dependencies:
dependencies
{
compile
com.getbase:floatingactionbutton:1.6.0
}
Una vez aadida la referencia a la librera, salvamos el fichero y nos aseguramos de pulsar
la opcin Sync Now que nos aparecer en la parte superior derecha del editor de cdigo:
55
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
Enlaces de inters:
Botones en Material Design
Botones en Gua de diseo Android
Botones en Gua de desarrollo Android
Librera futuresimple/android-floating-action-button (GitHub)
Librera makovkastar/FloatingActionButton (GitHub)
Blog Styling Android: Floating Action Button (Parte 1, 2, 3)
el
tamao
mximo
que
puede
ocupar
la
56
57
De igual forma, tambin podemos manipular estas propiedades desde nuestro cdigo.
Como ejemplo, en el siguiente fragmento recuperamos el texto de una etiqueta
con getText(), y posteriormente le concatenamos unos nmeros, actualizamos su contenido
mediante setText() y le cambiamos su color de fondo con setBackgroundColor().
1 final TextView lblEtiqueta = (TextView)findViewById(R.id.LblEtiqueta);
2 String texto = lblEtiqueta.getText().toString();
3 texto += "123";
4 lblEtiqueta.setText(texto);
5 lblEtiqueta.setBackgroundColor(Color.BLUE);
Control EditText [API]
El control EditText es el componente de edicin de texto que proporciona la plataforma
Android. Permite la introduccin y edicin de texto por parte del usuario, por lo que en
tiempo de diseo la propiedad ms interesante a establecer, adems de su posicin/tamao
y formato, es el texto a mostrar, atributoandroid:text. Por supuesto si no queremos que el
cuadro de texto aparezca inicializado con ningn texto, no es necesario incluir esta
propiedad en el layout XML. Lo que s deberemos establecer ser la
propiedad android:inputType. Esta propiedad indica el tipo de contenido que se va a
introducir en el cuadro de texto, como por ejemplo una direccin de correo electrnico
(textEmailAddress), un nmero genrico (number), un nmero de telfono (phone), una
direccin web (textUri), o un texto genrico (text). El valor que establezcamos para esta
propiedad tendr adems efecto en el tipo de teclado que mostrar Android para editar
dicho campo. As, por ejemplo, si hemos indicado text mostrar el teclado completo
alfanumrico, si hemos indicado phone mostrar el teclado numrico del telfono, etc.
1 <EditText android:id="@+id/TxtBasico"
2
android:layout_width="match_parent"
3
android:layout_height="wrap_content"
4
android:inputType="text" />
Al igual que ocurra con los botones, donde podamos indicar una imagen que acompaara
al texto del mismo, con los controles de texto podemos hacer lo mismo. Las
propiedades drawableLeft odrawableRight nos permite especificar una imagen, a izquierda
o derecha, que permanecer fija en el cuadro de texto.
Otra opcin adicional ser indicar un texto de ayuda o descripcin (hint), que aparecer en
el cuadro de texto mientras el usuario no haya escrito nada (en cuanto se escribe algo este
texto desaparece). Para esto utilizaremos las propiedades android:hint para indicar el texto
y android:textColorHint para indicar su color.
Veamos un ejemplo utilizando las propiedades anteriores:
1
<EditText android:id="@+id/TxtImagenHint"
58
2
android:layout_width="match_parent"
3
android:layout_height="wrap_content"
4
android:drawableLeft="@drawable/ic_usuario"
5
android:hint="@string/usuario"
6
android:textColorHint="#CFCFCF"
7
android:inputType="text" />
Y su aspecto sera el siguiente:
podemos
utilizar
Un detalle que puede haber pasado desapercibido. Os habis fijado en que hemos tenido
que hacer untoString() sobre el resultado de getText()? La explicacin para esto es que el
mtodo getText() no devuelve directamente una cadena de caracteres (String) sino un
objeto de tipo Editable, que a su vez implementa la interfaz Spannable. Y esto nos lleva a la
caracterstica ms interesante del controlEditText, y es que no slo nos permite editar texto
plano sino tambin texto enriquecido o con formato. Veamos cmo y qu opciones tenemos
disponibles, y para empezar comentemos algunas cosas sobre los objetos Spannable.
Interfaz Spanned
Un objeto de tipo Spanned es algo as como una cadena de caracteres (de hecho deriva de la
interfazCharSequence) en la que podemos insertar otros objetos a modo de marcas o
etiquetas(spans) asociados a rangos de caracteres. De esta interfaz deriva la
interfaz Spannable, que permite la modificacin de estas marcas, y a su vez de sta ltima
deriva la interfaz Editable, que permite adems la modificacin del texto.
Aunque en el apartado en el que nos encontramos nos interesaremos principalmente por las
marcas de formato de texto, en principio podramos insertar cualquier tipo de objeto.
Existen muchos tipos de spans predefinidos en la plataforma que podemos utilizar para dar
formato al texto, entre ellos:
TypefaceSpan. Modifica el tipo de fuente.
StyleSpan. Modifica el estilo del texto (negrita, cursiva, ).
ForegroudColorSpan. Modifica el color del texto.
AbsoluteSizeSpan. Modifica el tamao de fuente.
De esta forma, para crear un nuevo objeto Editable e insertar una marca de formato
podramos hacer lo siguiente:
1 //Creamos un nuevo objeto de tipo Editable
59
2
3
4
5
En este ejemplo estamos insertando un span de tipo StyleSpan para marcar un fragmento de
texto con estilo negrita. Para insertarlo utilizamos el mtodo setSpan(), que recibe como
parmetro el objeto Spana insertar, la posicin inicial y final del texto a marcar, y
un flag que indica la forma en la que el span se podr extender al insertarse nuevo texto.
Texto con formato en controles TextView y EditText
Hemos visto cmo crear un objeto Editable y aadir marcas de formato al texto que
contiene, pero todo esto no tendra ningn sentido si no pudiramos visualizarlo. Como ya
podis imaginar, los controlesTextView y EditText nos van a permitir hacer esto. Veamos
qu ocurre si asignamos a nuestro controlEditText el objeto Editable que hemos creado
antes:
1 txtTexto.setText(str);
Tras ejecutar este cdigo, para lo que hemos insertado un botn SetText en la aplicacin
de ejemplo, veremos como efectivamente en el cuadro de texto aparece el mensaje con el
formato esperado:
En la aplicacin de ejemplo tambin he incluido un botn adicional Negrita que se
encargar de convertir a estilo negrita un fragmento de texto previamente seleccionado en
el cuadro de texto. Mi intencin con esto es presentar los mtodos disponibles para
determinar el comienzo y el fin de una seleccin en un control de este tipo. Para ello
utilizaremos los mtodos getSelectionStart() y getSelectionEnd(), que nos devolvern el
ndice del primer y ltimo carcter seleccionado en el texto. Sabiendo esto, ya slo nos
queda utilizar el mtodo setSpan() que ya conocemos para convertir la seleccin a negrita.
1 Spannable texto = txtTexto.getText();
2
3 int ini = txtTexto.getSelectionStart();
4 int fin = txtTexto.getSelectionEnd();
5
6 texto.setSpan(
7
new StyleSpan(android.graphics.Typeface.BOLD),
8
ini, fin,
9
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Bien, ya hemos visto cmo asignar texto con y sin formato a un cuadro de texto, pero qu
ocurre a la hora de recuperar texto con formato desde el control? Ya vimos que la
funcin getText() devuelve un objeto de tipo Editable y que sobre ste podamos hacer
11,
19
60
un toString(). Pero con esta solucin estamos perdiendo todo el formato del texto, por lo
que no podramos por ejemplo salvarlo a una base de datos.
La solucin a esto ltimo pasa obviamente por recuperar directamente el objeto Editable y
serializarlo de algn modo, mejor an si es en un formato estandar. Pues bien, en Android
este trabajo ya nos viene hecho de fbrica a travs de la clase Html [API], que dispone de
mtodos para convertir cualquier objetoSpanned en su representacin HTML equivalente.
Veamos cmo. Recuperemos el texto de la ventana anterior y utilicemos el
mtodo Html.toHtml(Spannable) para convertirlo a formato HTML:
1 //Obtiene el texto del control con etiquetas de formato HTML
2 String aux2 = Html.toHtml(txtTexto.getText());
Haciendo esto, obtendramos una cadena de texto como la siguiente, que ya podramos por
ejemplo almacenar en una base de datos o publicar en cualquier web sin perder el formato
de texto establecido:
1
<p>Esto es un <b>simulacro</b>.</p>
Enlaces de inters:
Campos de texto en Material Design
Campos de texto en Gua de diseo Android
Campos de texto en Gua de desarrollo Android
Presentacin Advanced Android TextView (Chiu-Ki Chan)
Artculo Spans, a Powerful Concept (Flavien Laurent)
Implementacin de Floating Label: post y cdigo (Chris Banes)
Librera Calligraphy (fuentes personalizadas)
Interfaz de usuario en Android: Controles bsicos (III)
61
62
8
if (isChecked) {
9
cbMarcame.setText("Checkbox marcado!");
10
}
11
else {
12
cbMarcame.setText("Checkbox desmarcado!");
13
}
14
}
15 });
Otro evento que podramos utilizar es onCheckedChanged, que nos informa de que ha
cambiado el estado del control. Para implementar las acciones de este evento podramos
utilizar la siguiente lgica, donde tras capturar el evento, y dependiendo del nuevo estado
del control (variable isChecked recibida como parmetro), haremos una accin u otra:
cbMarcame = (CheckBox)findViewById(R.id.ChkMarcame);
1
2
cbMarcame.setOnCheckedChangeListener(new
3
CheckBox.OnCheckedChangeListener() {
4
public void onCheckedChanged(CompoundButton buttonView, boolean
5
isChecked) {
6
if (isChecked) {
7
cbMarcame.setText("Checkbox marcado!");
8
}
9
else {
10
cbMarcame.setText("Checkbox desmarcado!");
11
}
12
}
});
Control RadioButton [API]
Al igual que los controles checkbox, un radio button puede estar marcado o desmarcado,
pero en este caso suelen utilizarse dentro de un grupo de opciones donde una, y slo una, de
ellas debe estar marcada obligatoriamente, es decir, que si se marca una de las opciones se
desmarcar automticamente la que estuviera activa anteriormente. En Android, un grupo
de botones radio button se define mediante un elemento RadioGroup, que a su vez
contendr todos los elementos RadioButton necesarios. Veamos un ejemplo de cmo
definir un grupo de dos controles radiobutton en nuestra interfaz:
1
<RadioGroup android:id="@+id/GrbGrupo1"
2
android:orientation="vertical"
3
android:layout_width="match_parent"
4
android:layout_height="match_parent" >
5
6
<RadioButton android:id="@+id/RbOpcion1"
7
android:layout_width="wrap_content"
8
android:layout_height="wrap_content"
9
android:text="@string/opcion_1" />
63
10
11
<RadioButton android:id="@+id/RbOpcion2"
12
android:layout_width="wrap_content"
13
android:layout_height="wrap_content"
14
android:text="@string/opcion_2" />
15 </RadioGroup>
En primer lugar vemos cmo podemos definir el grupo de controles indicando su
orientacin (vertical u horizontal) al igual que ocurra por ejemplo con un LinearLayout.
Tras esto, se aaden todos los objetosRadioButton necesarios indicando su ID mediante la
propiedad android:id y su texto medianteandroid:text.
Una vez definida la interfaz podremos manipular el control desde nuestro cdigo java
haciendo uso de los diferentes mtodos del control RadioGroup, los ms
importantes: check(id) para
marcar
una
opcin
determinada
mediante
su
ID, clearCheck() para desmarcar todas las opciones, ygetCheckedRadioButtonId() que
como su nombre indica devolver el ID de la opcin marcada (o el valor -1 si no hay
ninguna marcada). Veamos un ejemplo:
1 RadioGroup rg = (RadioGroup)findViewById(R.id.GrbGrupo1);
2 rg.clearCheck();
3 rg.check(R.id.RbOpcion1);
4 int idSeleccionado = rg.getCheckedRadioButtonId();
En cuanto a los eventos lanzados, recurriremos nuevamente al evento onClick para saber
cundo se pulsa cada uno de los botones del grupo. Normalmente utilizaremos un
mismo listener para todos los radiobutton del grupo, por lo que lo definiremos de forma
independiente y despus lo asignaremos a todos los botones.
1
private TextView lblMensaje;
2
private RadioButton rbOpcion1;
3
private RadioButton rbOpcion2;
4
5
//...
6
7
lblMensaje = (TextView)findViewById(R.id.LblSeleccion);
8
rbOpcion1 = (RadioButton)findViewById(R.id.RbOpcion1);
9
rbOpcion2 = (RadioButton)findViewById(R.id.RbOpcion2);
10
11 View.OnClickListener list = new View.OnClickListener() {
12
@Override
13
public void onClick(View view) {
14
String opcion = "";
15
switch(view.getId()) {
16
case R.id.RbOpcion1:
17
opcion = "opcin 1";
18
break;
64
19
20
21
22
23
24
25
26
27
28
29
case R.id.RbOpcion2:
opcion = "opcin 2";
break;
}
lblMensaje.setText("ID opcin seleccionada: " + opcion);
}
};
rbOpcion1.setOnClickListener(list);
rbOpcion2.setOnClickListener(list);
65
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
Enlaces de inters:
Switches en Material Design
Switches en Gua de diseo Android
CheckBoxes y RadioButton en Gua de desarrollo Android.
Interfaz de usuario en Android: Controles de seleccin (I)
by Sgoliver on 07/09/2010 in Android, Programacin
Una vez repasados los controles bsicos (I, II, III) que podemos utilizar en nuestras
aplicaciones Android, vamos a dedicar los prximos artculos a describir los diferentes
controles de seleccin disponibles en la plataforma. Al igual que en
otros frameworks Android dispone de diversos controles que nos permiten seleccionar una
opcin dentro de una lista de posibilidades. As, podremos utilizar por ejemplo listas
desplegables (Spinner), listas fijas (ListView), o tablas (GridView).
En este primer artculo dedicado a los controles de seleccin vamos a describir un elemento
importante y comn a todos ellos, los adaptadores, y lo vamos a aplicar al primer control
de los indicados, las listas desplegables.
Adaptadores en Android (adapters)
Para los desarrolladores de java que hayan utilizado frameworks de interfaz grfica
como Swing, el concepto de adaptador les resultar familiar. Un adaptador representa algo
as como una interfaz comn al modelo de datos que existe por detrs de todos los controles
de seleccin que hemos comentado. Dicho de otra forma, todos los controles de seleccin
accedern a los datos que contienen a travs de un adaptador.
Adems de proveer de datos a los controles visuales, el adaptador tambin ser responsable
de generar a partir de estos datos las vistas especficas que se mostrarn dentro del control
de seleccin. Por ejemplo, si cada elemento de una lista estuviera formado a su vez por una
66
1. El contexto, que normalmente ser simplemente una referencia a la actividad donde se crea
el adaptador.
2. El ID del layout sobre el que se mostrarn los datos del control. En este caso le pasamos el
ID de un layout predefinido en Android (android.R.layout.simple_spinner_item), formado
nicamente por un control TextView, pero podramos pasarle el ID de cualquier layout
personalizado de nuestro proyecto con cualquier estructura y conjunto de controles, ms
adelante veremos cmo (en el apartado dedicado a las listas fijas).
3. El array que contiene los datos a mostrar.
Con esto ya tendramos creado nuestro adaptador para los datos a mostrar y ya tan slo nos
quedara asignar este adaptador a nuestro control de seleccin para que ste mostrase los
datos en la aplicacin.
67
Una alternativa a tener en cuenta si los datos a mostrar en el control son estticos sera
definir la lista de posibles valores como un recurso de tipo string-array. Para ello, primero
crearamos un nuevo fichero XML en la carpeta /res/values llamado por
ejemplo valores_array.xml e incluiramos en l los valores seleccionables de la siguiente
forma:
1
<?xml version="1.0" encoding="utf-8"?>
2
<resources>
3
<string-array name="valores_array">
4
<item>Elem1</item>
5
<item>Elem2</item>
6
<item>Elem3</item>
7
<item>Elem4</item>
8
<item>Elem5</item>
9
</string-array>
10 </resources>
Tras
esto,
a
la
hora
de
crear
el
adaptador,
utilizaramos
el
mtodo createFromResource() para hacer referencia a este array XML que acabamos de
crear:
1 ArrayAdapter<CharSequence> adapter =
2
ArrayAdapter.createFromResource(this,
3
R.array.valores_array,
4
android.R.layout.simple_spinner_item);
Control Spinner [API]
Las listas desplegables en Android se llaman Spinner. Funcionan de forma similar a
cualquier control de este tipo, el usuario selecciona la lista, se muestra una especie de lista
emergente al usuario con todas las opciones disponibles y al seleccionarse una de ellas sta
queda fijada en el control. Para aadir una lista de este tipo a nuestra aplicacin podemos
utilizar el cdigo siguiente:
1
2
3
<Spinner android:id="@+id/CmbOpciones"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Poco vamos a comentar de aqu ya que lo que nos interesan realmente son los datos a
mostrar. En cualquier caso, las opciones para personalizar el aspecto visual del control
(fondo, color y tamao de fuente, ) son las mismas ya comentadas para los controles
bsicos.
Para enlazar nuestro adaptador (y por tanto nuestros datos) a este control utilizaremos el
siguiente cdigo java:
68
1
2
3
4
5
6
7
8
9
10
Comenzamos como siempre por obtener una referencia al control a travs de su ID. Y en la
ltima lnea asignamos el adaptador al control mediante el mtodo setAdapter(). Y la
segunda lnea para qu es? Cuando indicamos en el apartado anterior cmo construir un
adaptador vimos cmo uno de los parmetros que le pasbamos era el ID del layout que
utilizaramos para visualizar los elementos del control. Sin embargo, en el caso del
control Spinner, este layout tan slo se aplicar al elemento seleccionado en la lista, es
decir, al que se muestra directamente sobre el propio control cuando no est desplegado.
Sin embargo, antes indicamos que el funcionamiento normal del control Spinner incluye
entre otras cosas mostrar una lista emergente con todas las opciones disponibles. Pues bien,
para personalizar tambin el aspecto de cada elemento en dicha lista emergente tenemos el
mtodosetDropDownViewResource(ID_layout), al que podemos pasar otro ID de layout
distinto al primero sobre el que se mostrarn los elementos de la lista emergente. En este
caso hemos utilizado otro layout predefinido an Android para las listas desplegables
(android.R.layout.simple_spinner_dropdown_item), formado por una etiqueta de texto con
la descripcin de la opcin (en Android 2.x tambin se muestra un marcador circular a la
derecha que indica si la opcin est o no seleccionada).
Con estas simples lineas de cdigo conseguiremos mostrar un control como el que vemos
en la siguiente imagen:
69
En cuanto a los eventos lanzados por el control Spinner, el ms comunmente utilizado ser
el generado al seleccionarse una opcin de la lista desplegable, onItemSelected. Para
capturar este evento se proceder de forma similar a lo ya visto para otros controles
anteriormente, asignadole su controlador mediante el mtodo setOnItemSelectedListener():
1
cmbOpciones.setOnItemSelectedListener(
2
new AdapterView.OnItemSelectedListener() {
3
public void onItemSelected(AdapterView<?> parent,
4
android.view.View v, int position, long id) {
5
lblMensaje.setText("Seleccionado: " +
6
parent.getItemAtPosition(position));
7
}
8
9
public void onNothingSelected(AdapterView<?> parent) {
10
lblMensaje.setText("");
11
}
12
});
A diferencia de ocasiones anteriores, para este evento definimos dos mtodos, el primero de
ellos (onItemSelected) que ser llamado cada vez que se selecciones una opcin en la lista
desplegable, y el segundo (onNothingSelected) que se llamar cuando no haya ninguna
opcin seleccionada (esto puede ocurrir por ejemplo si el adaptador no tiene
datos). Adems, vemos cmo para recuperar el dato seleccionado utilizamos el
mtodo getItemAtPosition() del parmetro AdapterView que recibimos en el evento.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
En el siguiente artculo describiremos el uso de controles de tipo lista (ListView).
70
de
Android
para
los
controles
de
71
formado
nicamente
por
Como podis comprobar el uso bsico del control ListView es completamente anlogo al
ya comentado para el control Spinner.
Hasta aqu todo sencillo. Pero, y si necesitamos mostrar datos ms complejos en la lista?
qu ocurre si necesitamos que cada elemento de la lista est formado a su vez por varios
elementos? Pues vamos a provechar este artculo dedicado a los ListView para ver cmo
podramos conseguirlo, aunque todo lo que comentar es extensible a otros controles de
seleccin.
Para no complicar mucho el tema vamos a hacer que cada elemento de la lista muestre por
ejemplo dos lneas de texto a modo de ttulo y subttulo con formatos diferentes (por
supuesto se podran aadir muchos ms elementos, por ejemplo imgenes, checkboxes, etc).
En primer lugar vamos a crear una nueva clase java para contener nuestros datos de prueba.
Vamos a llamarla Titular y tan slo va a contener dos atributos, ttulo y subttulo.
1
public class Titular
2
{
3
private String titulo;
4
private String subtitulo;
5
6
public Titular(String tit, String sub){
7
titulo = tit;
8
subtitulo = sub;
9
}
10
11
public String getTitulo(){
12
return titulo;
13
}
72
14
15
16
17
18
En cada elemento de la lista queremos mostrar ambos datos, por lo que el siguiente paso
ser crear un layout XML con la estructura que deseemos. En mi caso voy a mostrarlos en
dos etiquetas de texto (TextView), la primera de ellas en negrita y con un tamao de letra
un poco mayor. Llamaremos a este layout listitem_titular.xml:
1
<LinearLayout
2
xmlns:android="http://schemas.android.com/apk/res/android"
3
android:layout_width="wrap_content"
4
android:layout_height="wrap_content"
5
android:orientation="vertical">
6
7
<TextView android:id="@+id/LblTitulo"
8
android:layout_width="match_parent"
9
android:layout_height="wrap_content"
10
android:textStyle="bold"
11
android:textSize="20sp" />
12
13 <TextView android:id="@+id/LblSubTitulo"
14
android:layout_width="match_parent"
15
android:layout_height="wrap_content"
16
android:textStyle="normal"
17
android:textSize="12sp" />
18
19 </LinearLayout>
Ahora que ya tenemos creados tanto el soporte para nuestros datos como el layout que
necesitamos para visualizarlos, lo siguiente que debemos hacer ser indicarle al adaptador
cmo debe utilizar ambas cosas para generar nuestra interfaz de usuario final. Para ello
vamos a crear nuestro propio adaptador extendiendo de la clase ArrayAdapter.
1
class AdaptadorTitulares extends ArrayAdapter<Titular> {
2
3
public AdaptadorTitulares(Context context, Titular[] datos) {
4
super(context, R.layout.listitem_titular, datos);
5
}
6
7
public View getView(int position, View convertView, ViewGroup parent) {
8
LayoutInflater inflater = LayoutInflater.from(getContext());
9
View item = inflater.inflate(R.layout.listitem_titular, null);
10
11
TextView lblTitulo = (TextView)item.findViewById(R.id.LblTitulo);
12
lblTitulo.setText(datos[position].getTitulo());
73
13
14
15
16
17
18
19
74
13
14
15
16
17
Hecho esto, y si todo ha ido bien, nuestra nueva lista debera quedar como vemos en la
imagen siguiente:
Otra posible personalizacin de nuestra lista podra ser aadirle una cabecera o un pie. Para
esto, definiremos un nuevo layout XML para la cabecera/pie y lo aadiremos a la lista antes
de asignar el adaptador.
As por ejemplo, podramos crear la siguiente cabecera compuesta por una etiqueta de texto
centrada y en negrita sobre fondo azul, en un fichero XML situado
en layout/list_header.xml:
1
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
android:orientation="vertical" android:layout_width="match_parent"
3
android:layout_height="match_parent">
4
5
<TextView
6
android:layout_width="match_parent"
7
android:layout_height="40dp"
8
android:text="CABECERA"
9
android:textStyle="bold"
10
android:background="#ff0093ff"
11
android:gravity="center" />
12
13 </LinearLayout>
75
Por ltimo comentemos un poco los eventos de este tipo de controles. Si quisiramos
realizar cualquier accin al pulsarse sobre un elemento de la lista creada tendremos que
implementar el eventoonItemClick. Veamos cmo con un ejemplo:
1
lstOpciones.setOnItemClickListener(new AdapterView.OnItemClickListener() {
2
public void onItemClick(AdapterView<?> a, View v, int position, long id) {
3
4
//Alternativa 1:
5
String opcionSeleccionada =
6
((Titular)a.getItemAtPosition(position)).getTitulo();
7
8
//Alternativa 2:
9
//String opcionSeleccionada =
10
// ((TextView)v.findViewById(R.id.LblTitulo))
11
//
.getText().toString();
12
13
lblEtiqueta.setText("Opcin seleccionada: " + opcionSeleccionada);
14
}
76
15 });
Este evento recibe 4 parmetros:
Con todos estos datos, si quisiramos por ejemplo mostrar el ttulo de la opcin pulsada en
la etiqueta de texto superior (lblEtiqueta) tendramos dos posibilidades:
1. Acceder a la vista asociada al adaptador y a partir de sta obtener
mediante getItemAtPosition()el elemento cuya posicin sea position. Esto nos devolvera
un objeto de tipo Titular, por lo que obtendramos el ttulo llamando a su
mtodo getTitulo().
2. Acceder directamente a la vista que se ha pulsado, que tendra la estructura definida en
nuestro
layout
personalizado listitem_titular.xml,
y
obtener
mediante findViewById() y getText() el texto del control que alberga el campo ttulo.
Y esto sera todo por ahora. Aunque ya sabemos utilizar y personalizar las listas en
Android, en el prximo apartado daremos algunas indicaciones para utilizar de una forma
mucho ms eficiente los controles de este tipo, algo que los usuarios de nuestra aplicacin
agradecern enormemente al mejorarse la respuesta de la aplicacin y reducirse el consumo
de batera.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
Interfaz de usuario en Android: Controles de seleccin (III)
by Sgoliver on 10/09/2010 in Android, Programacin
En el artculo anterior ya vimos cmo utilizar los controles de tipo ListView en Android.
Sin embargo, acabamos comentando que exista una forma ms eficiente de hacer uso de
dicho control, de forma que la respuesta de nuestra aplicacin fuera ms agil y se reduciese
el consumo de batera, algo que en plataformas mviles siempre es importante.
Como base para este artculo vamos a utilizar como cdigo que ya escribimos en el artculo
anterior, por lo que si has llegado hasta aqu directamente te recomiendo que leas primero
el primer post dedicado al control ListView.
Cuando comentamos cmo crear nuestro propio adaptador, extendiendo de ArrayAdapter,
para personalizar la forma en que nuestros datos se iban a mostrar en la lista escribimos el
siguiente cdigo:
1
class AdaptadorTitulares extends ArrayAdapter<Titular> {
77
2
3
public AdaptadorTitulares(Context context, Titular[] datos) {
4
super(context, R.layout.listitem_titular, datos);
5
}
6
7
public View getView(int position, View convertView, ViewGroup parent) {
8
LayoutInflater inflater = LayoutInflater.from(getContext());
9
View item = inflater.inflate(R.layout.listitem_titular, null);
10
11
TextView lblTitulo = (TextView)item.findViewById(R.id.LblTitulo);
12
lblTitulo.setText(datos[position].getTitulo());
13
14
TextView lblSubtitulo = (TextView)item.findViewById(R.id.LblSubTitulo);
15
lblSubtitulo.setText(datos[position].getSubtitulo());
16
17
return(item);
18
}
19 }
Centrndonos en la definicin del mtodo getView() vimos que la forma normal de
proceder consista en primer lugar en inflar nuestro layout XML personalizado para crear
todos los objetos correspondientes (con la estructura descrita en el XML) y posteriormente
acceder a dichos objetos para modificar sus propiedades. Sin embargo, hay que tener en
cuenta que esto se hace todas y cada una de las veces que se necesita mostrar un elemento
de la lista en pantalla, se haya mostrado ya o no con anterioridad, ya que Android no
guarda los elementos de la lista que desaparecen de pantalla (por ejemplo al hacer scroll
sobre la lista). El efecto de esto es obvio, dependiendo del tamao de la lista y sobre todo
de la complejidad del layout que hayamos definido esto puede suponer la creacin y
destruccin de cantidades ingentes de objetos (que puede que ni siquiera nos sean
necesarios), es decir, que la accin de inflar un layout XML puede ser bastante costosa, lo
que podra aumentar mucho, y sin necesidad, el uso de CPU, de memoria, y de batera.
Para aliviar este problema, Android nos propone un mtodo que permite reutilizar algn
layout que ya hayamos inflado con anterioridad y que ya no nos haga falta por algn
motivo, por ejemplo porque el elemento correspondiente de la lista ha desaparecido de la
pantalla al hacer scroll. De esta forma evitamos todo el trabajo de crear y estructurar todos
los objetos asociados al layout, por lo que tan slo nos quedara obtener la referencia a ellos
mediante findViewById() y modificar sus propiedades.
Pero cmo podemos reutilizar estos layouts obsoletos? Pues es bien sencillo, siempre
que exista algn layout que pueda ser reutilizado ste se va a recibir a travs del
parmetro convertView del mtodogetView(). De esta forma, en los casos en que ste no
78
sea null podremos obviar el trabajo de inflar el layout. Veamos cmo quedara el
mtod getView() tras esta optimizacin:
1
public View getView(int position, View convertView, ViewGroup parent)
2
{
3
View item = convertView;
4
5
if(item == null)
6
{
7
LayoutInflater inflater = context.getLayoutInflater();
8
item = inflater.inflate(R.layout.listitem_titular, null);
9
}
10
11
TextView lblTitulo = (TextView)item.findViewById(R.id.LblTitulo);
12
lblTitulo.setText(datos[position].getTitulo());
13
14
TextView lblSubtitulo = (TextView)item.findViewById(R.id.LblSubTitulo);
15
lblSubtitulo.setText(datos[position].getSubtitulo());
16
17
return(item);
18 }
Si ejecutamos ahora la aplicacin podemos comprobar que al hacer scroll sobre la lista todo
sigue funcionando con normalidad, con la diferencia de que le estamos ahorrando gran
cantidad de trabajo a la CPU.
Pero vamos a ir un poco ms all. Con la optimizacin que acabamos de implementar
conseguimos ahorrarnos el trabajo de inflar el layout definido cada vez que se muestra un
nuevo elemento. Pero an hay otras dos llamadas relativamente costosas que se siguen
ejecutando en todas las llamadas. Me refiero a la obtencin de la referencia a cada uno de
los objetos a modificar mediante el mtodo findViewById(). La bsqueda por ID de un
control determinado dentro del rbol de objetos de un layout tambin puede ser una tarea
costosa dependiendo de la complejidad del propio layout. Por qu no aprovechamos que
estamos guardando un layout anterior para guardar tambin la referencia a los controles
que lo forman de forma que no tengamos que volver a buscarlos? Pues eso es exactamente
lo que vamos a hacer mediante lo que suelen llamar patrn ViewHolder.
Nuestra clase ViewHolder tan slo va a contener una referencia a cada uno de los controles
que tengamos que manipular de nuestro layout, en nuestro caso las dos etiquetas de texto.
Definamos por tanto esta clase de la siguiente forma:
1 static class ViewHolder {
2
TextView titulo;
3
TextView subtitulo;
4 }
79
La idea ser por tanto crear e inicializar el objeto ViewHolder la primera vez que inflemos
un elemento de la lista y asociarlo a dicho elemento de forma que posteriormente podamos
recuperarlo fcilmente. Pero dnde lo guardamos? Fcil, en Android todos los controles
tienen una propiedad llamada Tag (podemos asignarla y recuperarla mediante los
mtodos setTag() y getTag() respectivamente) que puede contener cualquier tipo de objeto,
por lo que resulta ideal para guardar nuestro objeto ViewHolder. De esta forma, cuando el
parmetro convertView llegue informado sabremos que tambin tendremos disponibles las
referencias a sus controles hijos a travs de la propiedad Tag. Veamos el cdigo modificado
de getView() para aprovechar esta nueva optimizacin:
1
public View getView(int position, View convertView, ViewGroup parent)
2
{
3
View item = convertView;
4
ViewHolder holder;
5
6
if(item == null)
7
{
8
LayoutInflater inflater = context.getLayoutInflater();
9
item = inflater.inflate(R.layout.listitem_titular, null);
10
11
holder = new ViewHolder();
12
holder.titulo = (TextView)item.findViewById(R.id.LblTitulo);
13
holder.subtitulo = (TextView)item.findViewById(R.id.LblSubTitulo);
14
15
item.setTag(holder);
16
}
17
else
18
{
19
holder = (ViewHolder)item.getTag();
20
}
21
22
holder.titulo.setText(datos[position].getTitulo());
23
holder.subtitulo.setText(datos[position].getSubtitulo());
24
25
return(item);
26 }
Con estas dos optimizaciones hemos conseguido que la aplicacin sea mucho ms
respetuosa con los recursos del dispositivo de nuestros usuarios, algo que sin duda nos
agradecern.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
80
81
4
5
6
7
8
9
10
11
Por
supuesto, si no caben todos en la pantalla se podr hacer scroll sobre la tabla. Vemos en
una imagen cmo queda nuestra aplicacin de prueba:
82
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
Controles de seleccin (V): RecyclerView
by Admin on 24/02/2015 in Android, Programacin
Con la llegada de Android 5.0, Google ha incorporado al SDK de Android un nuevo
componente que viene a mejorar a los clsicos ListView y GridView existentes desde hace
tiempo. Probablemente no slo venga a mejorarlos sino tambin a sustituirlos en la mayora
de ocasiones ya que aporta flexibilidad de sobra para suplir la funcionalidad de ambos
controles e ir incluso ms all.
Este nuevo control se llama RecyclerView y, al igual que consegumos
83
84
85
19
public void bindTitular(Titular t) {
20
txtTitulo.setText(t.getTitulo());
21
txtSubtitulo.setText(t.getSubtitulo());
22
}
23
}
24
25
//...
26 }
Finalizado nuestro ViewHolder podemos ya seguir con la implementacin del adaptador
sobrescribiendo los mtodos indicados. En el mtodo onCreateViewHolder() nos
limitaremos a inflar una vista a partir del layout correspondiente a los elementos de la lista
(listitem_titular), y crear y devolver un nuevo ViewHolderllamando al constructor de
nuestra clase TitularesViewHolder pasndole dicha vista como parmetro.
Los dos mtodos restantes son an ms sencillos. En onBindViewHolder() tan slo
tendremos que recuperar el objeto Titular correspondiente a la posicin recibida como
parmetro y asignar sus datos sobre el ViewHolder tambin recibido como parmetro. Por
su parte, getItemCount() tan slo devolver el tamao de la lista de datos.
1
public class AdaptadorTitulares
2
extends RecyclerView.Adapter<AdaptadorTitulares.TitularesViewHolder> {
3
4
private ArrayList<Titular> datos;
5
6
//...
7
8
public AdaptadorTitulares(ArrayList<Titular> datos) {
9
this.datos = datos;
10
}
11
12
@Override
13
public TitularesViewHolder onCreateViewHolder(ViewGroup viewGroup, int
14 viewType) {
15
16
View itemView = LayoutInflater.from(viewGroup.getContext())
17
.inflate(R.layout.listitem_titular, viewGroup, false);
18
19
TitularesViewHolder tvh = new TitularesViewHolder(itemView);
20
21
return tvh;
22
}
23
24
@Override
25
public void onBindViewHolder(TitularesViewHolder viewHolder, int pos) {
26
Titular item = datos.get(pos);
27
86
28
29
30
31
32
33
34
35
36
viewHolder.bindTitular(item);
}
@Override
public int getItemCount() {
return datos.size();
}
//...
}
87
88
El siguiente paso que nos podemos plantear es cmo responder a los eventos que se
produzcan sobre elRecyclerView, como opcin ms habitual el evento click sobre un
89
90
}
Como vemos, nuestro adaptador implementar la interfaz OnClickListener, declarar un
listener (el que podremos asignar posteriormente desde fuera del adaptador) como atributo
de la clase, en el momento de crear el nuevo ViewHolder asociar el evento a la vista, y por
ltimo implementar el evento onClick, que se limitar a lanzar el mismo evento sobre el
listener externo. El mtodo adicional setOnClickListener()nos serivir para asociar el
listener real a nuestro adaptador en el momento de crearlo. Veamos cmo quedara nuestra
actividad principal con este cambio:
public class MainActivity extends ActionBarActivity {
1
2
//...
3
4
@Override
5
protected void onCreate(Bundle savedInstanceState) {
6
7
//...
8
9
recView = (RecyclerView) findViewById(R.id.RecView);
10
recView.setHasFixedSize(true);
11
12
final AdaptadorTitulares adaptador = new AdaptadorTitulares(datos);
13
14
adaptador.setOnClickListener(new View.OnClickListener() {
15
@Override
16
public void onClick(View v) {
17
Log.i("DemoRecView", "Pulsado el elemento " + recView.getChildPosition(v));
18
}
19
});
20
21
recView.setAdapter(adaptador);
22
23
recView.setLayoutManager(new
24
LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));
25
26
//...
27
}
28
}
En este caso al pulsar sobre un elemento de la lista tan slo escribiremos un mensaje de log
indicando la posicin en la lista del elemento pulsado, lo que conseguimos llamando al
mtodo getChildPosition()del RecyclerView.
Con esto ya podramos volver a ejecutar la aplicacin de ejemplo y comprobar si todo
funciona correctamente segn lo definido al pulsar sobre los elementos de la lista.
91
Por ltimo vamos a describir brevemente los dos componentes restantes relacionados
con RecyclerView.
En primer lugar nos ocuparemos de ItemDecoration. Los ItemDecoration nos servirn para
personalizar el aspecto de un RecyclerView ms all de la distribucin de los elementos en
pantalla. El ejemplo tpico de esto son los separadores o divisores de una
lista. RecyclerView no tiene ninguna propiedad divider como en el caso del ListView, por
lo que dicha funcionalidad debemos suplirla con un ItemDecoration.
Crear un ItemDecoration personalizado no es una tarea demasiado fcil, o al menos no
inmediata, por lo que por ahora no nos detendremos mucho en ello. Para el caso de los
separadores de lista podemos encontrar en un ejemplo del propio SDK de
Android un ejemplo de ItemDecoration que lo implementa. La clase en cuestin se
llama DividerItemDecoration y la incluyo en el proyecto de ejemplo de este
apartado (tenis el enlace a github al final del artculo). Si estudiamos un poco el cdigo
veremos que tendremos que extender de la clase RecyclerView.ItemDecoration e
implementar sus mtodosgetItemOffsets() y onDraw()/onDrawOver(). El primero de ellos
se encargar de definir los lmites del elemento de la lista y el segundo de dibujar el
elemento de personalizacin en s. En el fondo no es complicado, aunque s lleva su trabajo
construirlo desde cero.
Para utilizar este componente deberemos simplemente crear el objeto y asociarlo a
nuestro RecyclerViewmediante addItemDecoration() en nuestra actividad principal:
1
public class MainActivity extends ActionBarActivity {
2
3
//...
4
5
@Override
6
protected void onCreate(Bundle savedInstanceState) {
7
8
//...
9
10
recView = (RecyclerView) findViewById(R.id.RecView);
11
recView.setHasFixedSize(true);
12
13
final AdaptadorTitulares adaptador = new AdaptadorTitulares(datos);
14
15
adaptador.setOnClickListener(new View.OnClickListener() {
16
@Override
17
public void onClick(View v) {
18
Log.i("DemoRecView", "Pulsado el elemento " + recView.getChildPosition(v));
19
}
20
});
21
92
22
23
24
25
26
27
28
29
30
31
32
recView.setAdapter(adaptador);
recView.setLayoutManager(
new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));
recView.addItemDecoration(
new DividerItemDecoration(this,DividerItemDecoration.VERTICAL_LIST));
//...
}
}
Como
nota
adicional
indicar
que
podremos
aadir
a
un RecyclerView tantos ItemDecoration como queramos, que se aplicarn en el mismo
orden que se hayan aadido con addItemDecoration().
Por ltimo hablemos muy brevemente de ItemAnimator. Un componente ItemAnimator nos
permitir definir las animaciones que mostrar nuestro RecyclerView al realizar las
acciones ms comunes sobre un elemento (aadir, eliminar, mover, modificar). Este
componente tampoco es sencillo de implementar, pero por suerte el SDK tambin
proporciona una implementacin por defecto que puede servirnos en la mayora de
ocasiones, aunque por supuesto podremos personalizar creando nuestro
propioItemAnimator. Esta implementacin por defecto se llama DefaultItemAnimator y
podemos asignarla alRecyclerView mediante el mtodo setItemAnimator().
1
public class MainActivity extends ActionBarActivity {
2
3
//...
4
5
@Override
6
protected void onCreate(Bundle savedInstanceState) {
7
8
//...
9
10
recView = (RecyclerView) findViewById(R.id.RecView);
11
recView.setHasFixedSize(true);
12
13
final AdaptadorTitulares adaptador = new AdaptadorTitulares(datos);
14
15
adaptador.setOnClickListener(new View.OnClickListener() {
16
@Override
17
public void onClick(View v) {
18
Log.i("DemoRecView", "Pulsado el elemento " + recView.getChildPosition(v));
19
}
20
});
21
93
22
23
24
25
26
27
28
29
30
31
32
33
34
recView.setAdapter(adaptador);
recView.setLayoutManager(
new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false));
recView.addItemDecoration(
new DividerItemDecoration(this,DividerItemDecoration.VERTICAL_LIST));
recView.setItemAnimator(new DefaultItemAnimator());
//..
}
}
Para probar su funcionamiento vamos a aadir a nuestro layout principal tres botones con
las opciones de aadir, eliminar y mover un elemento de la lista (concretamente el segundo
elemento de la lista). La implementacin de estos botones es bastante sencilla y la
incluiremos tambin en el onCreate() de la actividad principal:
1
btnInsertar = (Button)findViewById(R.id.BtnInsertar);
2
3
btnInsertar.setOnClickListener(new View.OnClickListener() {
4
@Override
5
public void onClick(View v) {
6
datos.add(1, new Titular("Nuevo titular", "Subtitulo nuevo titular"));
7
adaptador.notifyItemInserted(1);
8
}
9
});
10
11 btnEliminar = (Button)findViewById(R.id.BtnEliminar);
12
13 btnEliminar.setOnClickListener(new View.OnClickListener() {
14
@Override
15
public void onClick(View v) {
16
datos.remove(1);
17
adaptador.notifyItemRemoved(1);
18
}
19 });
20
21 btnMover = (Button)findViewById(R.id.BtnMover);
22
23 btnMover.setOnClickListener(new View.OnClickListener() {
24
@Override
25
public void onClick(View v) {
26
27
Titular aux = datos.get(1);
28
datos.set(1,datos.get(2));
94
29
30
31
32
33
datos.set(2,aux);
adaptador.notifyItemMoved(1, 2);
}
});
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
95
utiliza en muchas de sus aplicaciones, entre ellas Google Now, quiz la que ms a ayudado
a popularizar este componente.
Hasta la llegada de Android 5.0, para utilizar estas tarjetas en la interfaz de nuestras
aplicaciones tenamos que recurrir a libreras de terceros o bien trabajar un poco para
implementar nuestra propia versin. Sin embargo ahora las tenemos disponibles en forma
de nueva librera de soporte oficial, proporcionada junto al SDK de Android.
Para hacer uso de la librera tan slo tendremos que hacer referencia a ella en la seccin de
dependencias del fichero build.gradle del mdulo principal de la aplicacin:
1 dependencies {
2
...
3
compile 'com.android.support:cardview-v7:21.0.+'
4 }
Una vez incluida la referencia a la librera, la utilizacin del componente es muy sencilla.
Tan slo tendremos que aadir a nuestro layout XML un control de
tipo<android.support.v7.widget.CardView> y establecer algunas de sus propiedades
principales. Yo por ejemplo le asignar una altura de 200dp (layout_height), una anchura
que se ajuste a su control padre (layout_width), y un color de fondo amarillo estilo post-it
(cardBackgroundColor).
1
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
xmlns:card_view="http://schemas.android.com/apk/res-auto"
3
xmlns:tools="http://schemas.android.com/tools"
4
android:layout_width="match_parent"
5
android:layout_height="match_parent"
6
android:paddingLeft="@dimen/activity_horizontal_margin"
7
android:paddingRight="@dimen/activity_horizontal_margin"
8
android:paddingTop="@dimen/activity_vertical_margin"
9
android:paddingBottom="@dimen/activity_vertical_margin"
10
android:orientation="vertical"
11
tools:context=".MainActivity">
12
13
<android.support.v7.widget.CardView
14
android:id="@+id/card2"
15
android:layout_width="match_parent"
16
android:layout_height="200dp"
17
card_view:cardBackgroundColor="#fffffe91" >
18
19
<TextView
20
android:id="@+id/txt1"
21
android:layout_width="match_parent"
22
android:layout_height="wrap_content"
23
android:padding="10dp"
96
24
25
26
27
28
android:text="@string/tarjeta_1" />
</android.support.v7.widget.CardView>
</LinearLayout>
Por supuesto, como en el caso de cualquier otro contenedor (un CardView no es ms que
una extensin de FrameLayout con esquinas redondeadas y una sombra inferior), dentro de
un CardView podemos aadir todos los controles que necesitemos. Como podis ver en el
cdigo anterior, a modo de ejemplo he aadido tan solo una etiqueta de texto (TextView).
Si ejecutamos en este momento el ejemplo sobre un dispositivo o emulador con Android
4.x encontraremos el resultado que esperbamos:
Sin embargo, si hacemos lo mismo sobre Android 5.x el resultado ser el que se muestra
en la siguiente imagen:
97
en la forma de calcular y mostrar las sombras (ente otras cosas) en Android 5 respecto a
versiones anteriores. Estas diferencias de comportamiento entre versiones del sistema
operativo provocan que en casos como el mostrado para un CardView, los tamaos
efectivos del contenido de la tarjeta y los mrgenes o padding de la vista (utilizado en esta
ocasin para dibujar la sombra) no coincidan entre versiones, lo que nos puede llevar a
alguna que otra sorpresa. En este caso, la solucin es sencilla, y pasa por utilizar una
propiedad adicional de CardViewllamada cardUseCompatPadding. Dando un valor true a
esta propiedad conseguiremos que en en Android 5.x se aada un padding extra
al CardView de forma que su representacin en pantalla sea similar a la de versiones
anteriores.
1 <android.support.v7.widget.CardView
2
android:id="@+id/card2"
3
android:layout_width="match_parent"
4
android:layout_height="200dp"
5
card_view:cardBackgroundColor="#fffffe91"
6
card_view:cardUseCompatPadding="true" >
Con este simple cambio, conseguiremos un comportamiento muy similar tanto en Android
4 como en Android 5.
Adems del color de fondo que ya hemos comentado, tambin podremos definir la
elevacin de la tarjeta y el radio de las esquinas redondeadas, utilizando las
propiedades cardElevation y cardCornerRadiusrespectivamente.
No son muchas ms las opciones de personalizacin que nos ofrece CardView, pero con
poco esfuerzo deben ser suficientes para crear tarjetas ms sofisticadas, jugando por
supuesto tambin con el contenido de la tarjeta. Como ejemplo, si incluimos una imagen a
modo de fondo (ImageView), y una etiqueta de texto superpuesta y alineada al borde
inferior (layout_gravity=bottom) con fondo negro algo traslcido (por
ejemplo background=#8c000000) y un color y tamao de texto adecuados, podramos
conseguir una tarjeta con el aspecto siguiente en Android 5.x:
98
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<android.support.v7.widget.CardView
android:id="@+id/card1"
android:layout_width="match_parent"
android:layout_height="200dp"
card_view:cardCornerRadius="6dp"
card_view:cardElevation="10dp"
card_view:cardUseCompatPadding="true" >
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/city"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/txt2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/tarjeta_2"
android:layout_gravity="bottom"
android:background="#8c000000"
android:textColor="#ffe3e3e3"
android:textSize="30sp"
android:textStyle="bold"/>
</android.support.v7.widget.CardView>
Nueva sorpresa. Como podemos ver, en Android 4.x y anteriores la imagen no llega al
borde delCardView, sino que se introduce un pequeo margen adicional que evita que se
tengan que hacer las operaciones necesarias para redondear las esquinas de la imagen. Esta
operacin de redondeo, aunque a priori pueda parecer simple, es bastante costosa. En
Android 5.x puede realizarse sin afectar a la experiencia de usuario gracias a que muchas de
las operaciones necesarias para mostrar la interfaz grfica de la aplicacin (animaciones,
99
Enlaces de inters:
Cards en Material Design
Creating Lists and Cards en Gua de desarrollo Android
CardView en Referencia APIs
100
101
9
10
11
Por ltimo el paso ms importante. Dado que queremos modificar el aspecto del control
para aadir el contador de caracteres tendremos que sobreescribir el evento onDraw(), que
es llamado por Android cada vez que hay que redibujar el control en pantalla. Este mtodo
recibe como parmetro un objeto Canvas, que no es ms que el lienzo sobre el que
podemos dibujar todos los elementos extra necesarios en el control. El objeto Canvas,
proporciona una serie de mtodos para dibujar cualquier tipo de elemento (lineas,
rectngulos, elipses, texto, bitmaps, ) sobre el espacio ocupado por el control. En nuestro
caso tan slo vamos a necesitar dibujar sobre el control un rectngulo que sirva de fondo
para el contador y el texto del contador con el nmero de caracteres actual del cuadro de
texto. No vamos a entrar en muchos detalles sobre la forma de dibujar grficos, pero vamos
a ver al menos las acciones principales.
En primer lugar definiremos los pinceles (objetos Paint) que utilizaremos para dibujar,
uno de ellos (p1) de color negro y relleno slido para el fondo del contador, y otro (p2) de
color blanco para el texto. Para configurar los colores, el estilo de fondo y el tamao del
texto utilizaremos los mtodos setColor(),setStyle() y setTextSize() respectivamente:
1
private void inicializacion()
2
{
3
Paint p1 = new Paint(Paint.ANTI_ALIAS_FLAG);
4
p1.setColor(Color.BLACK);
5
p1.setStyle(Style.FILL);
6
7
Paint p2 = new Paint(Paint.ANTI_ALIAS_FLAG);
8
p2.setColor(Color.WHITE);
9
p2.setTextSize(20);
10 }
Dado que estos elementos tan slo har falta crearlos la primera vez que se dibuje el
control, para evitar trabajo innecesario no incluiremos su definicin en el
mtodo onDraw(), sino que los definiremos como atributos de la clase y los inicializaremos
en el constructor del control (en los tres).
Una vez definidos los diferentes pinceles necesarios, dibujaremos el fondo y el texto del
contador mediante los mtodos drawRect() y drawText(), respectivamente, del objeto
canvas recibido en el evento.
Lo nico a tener en cuenta es que todos estos mtodos de dibujo reciben las unidades en
pixeles y por tanto si utilizamos valores fijos tendremos problemas al visualizar los
resultados en pantallas con distintas densidades de pxeles. Para evitar esto en lo posible,
tendremos que convertir nuestros valores de pxeles a algn valor dependiente de la
102
caso
particular
103
Para finalizar, veamos cmo quedara nuestro control ejecutando la aplicacin de ejemplo
en el emulador:
104
105
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/BtnAceptar"
android:text="@string/login"
android:paddingLeft="20dp"
android:paddingRight="20dp" />
<TextView android:id="@+id/LblMensaje"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
106
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
107
Vamos a aadir ahora algo ms de funcionalidad. En primer lugar, podemos aadir algn
mtodo pblico exclusivo de nuestro control. Como ejemplo podemos aadir un mtodo
que permita modificar el texto de la etiqueta de resultado del login. Esto no tiene ninguna
dificultad:
1
public void setMensaje(String msg)
2
{
3
lblMensaje.setText(msg);
4
}
En segundo lugar, todo control que se precie debe tener algunos eventos que nos permitan
responder a las acciones del usuario de la aplicacin. As por ejemplo, los botones tienen un
evento OnClick, las listas un evento OnItemSelected, etc. Pues bien, nosotros vamos a
dotar a nuestro control de un evento personalizado, llamado OnLogin, que se lance cuando
el usuario introduce sus credenciales de identificacin y pulsa el botn Login.
Para ello, lo primero que vamos a hacer es concretar los detalles de dicho evento, creando
una interfaz java para definir su listener. Esta interfaz tan slo tendr un mtodo
llamado onLogin() que devolver los dos datos introducidos por el usuario (usuario y
contrasea). Vemos cmo queda:
1
package net.sgoliver.android.controlpers2;
2
3
public interface OnLoginListener
4
{
5
void onLogin(String usuario, String password);
6
}
A continuacin, deberemos aadir un nuevo miembro de tipo OnLoginListener a la
clase ControlLogin, y un mtodo pblico que permita suscribirse al nuevo evento.
1
public class ControlLogin extends LinearLayout
108
2
3
4
5
6
7
8
9
10
11
{
private OnLoginListener listener;
//...
public void setOnLoginListener(OnLoginListener l)
{
listener = l;
}
}
Para ello, implementaremos el evento OnClick como ya hemos hecho en otras ocasiones y
como acciones generaremos el evento OnLogin de nuestro listener pasndole los datos que
ha introducido el usuario en los cuadros de texto Usuario y Contrasea:
1
private void asignarEventos()
2
{
3
btnLogin.setOnClickListener(new OnClickListener()
4
{
5
@Override
6
public void onClick(View v) {
7
listener.onLogin(txtUsuario.getText().toString(),
8
txtPassword.getText().toString());
9
}
10
});
109
11
Con todo esto, la aplicacin principal ya puede implementar el evento OnLogin de nuestro
control, haciendo por ejemplo la validacin de las credenciales del usuario y modificando
convenientemente el texto de la etiqueta de resultado:
1
@Override
2
public void onCreate(Bundle savedInstanceState)
3
{
4
super.onCreate(savedInstanceState);
5
setContentView(R.layout.main);
6
7
ctlLogin = (ControlLogin)findViewById(R.id.CtlLogin);
8
9
ctlLogin.setOnLoginListener(new OnLoginListener()
10
{
11
@Override
12
public void onLogin(String usuario, String password)
13
{
14
//Validamos el usuario y la contrasea
15
if (usuario.equals("demo") && password.equals("demo"))
16
ctlLogin.setMensaje("Login correcto!");
17
else
18
ctlLogin.setMensaje("Vuelva a intentarlo.");
19
}
20
});
21
}
Veamos lo que ocurre al ejecutar ahora la aplicacin principal e introducir las credenciales
correctas:
110
ya que nuestro control derivaba de ste. Pero vamos a ir ms all y vamos a definir tambin
atributos XML exclusivos para nuestro control. Como ejemplo, vamos a definir un atributo
llamado login_text que permita establecer el texto del botn de Login desde el propio
layout XML, es decir, en tiempo de diseo.
Primero vamos de declarar el nuevo atributo y lo vamos a asociar al control ControlLogin.
Esto debe hacerse en el fichero \res\values\attrs.xml. Para ello, aadiremos una nueva
seccin <declare-styleable> asociada a ControlLogin dentro del elemento <resources>,
donde indicaremos el nombre (name) y el tipo (format) del nuevo atributo.
1
<resources>
2
<declare-styleable name="ControlLogin">
3
<attr name="login_text" format="string"/>
4
</declare-styleable>
5
</resources>
En nuestro caso, el tipo del atributo ser string, dado que contendr una cadena de texto con
el mensaje a mostrar en el botn.
Con esto ya tendremos permitido el uso del nuevo atributo dentro del layout de la
aplicacin principal. Ahora nos falta procesar el atributo desde nuestro control
personalizado. Este tratamiento lo podemos hacer en el construtor de la clase ControlLogin.
Para ello, obtendremos la lista de atributos asociados aControlLogin mediante el
mtodo obtainStyledAttributes() del contexto de la aplicacin, obtendremos el valor del
nuevo atributo definido (mediante su ID, que estar formado por la concatenacin del
nombre del control y el nombre del atributo, en nuestro caso ControlLogin_login_text) y
modificaremos el texto por defecto del control con el nuevo texto.
1
public ControlLogin(Context context, AttributeSet attrs) {
2
super(context, attrs);
3
inicializar();
4
5
// Procesamos los atributos XML personalizados
6
TypedArray a =
7
getContext().obtainStyledAttributes(attrs,
8
R.styleable.ControlLogin);
9
10
String textoBoton = a.getString(
11
R.styleable.ControlLogin_login_text);
12
13
btnLogin.setText(textoBoton);
14
15
a.recycle();
16
}
111
Ya slo nos queda utilizarlo. Para ello debemos primero declarar un nuevo espacio de
nombres (namespace) local para el paquete java utilizado, que en nuestro caso he llamado
sgo:
1
xmlns:sgo="http://schemas.android.com/apk/res-auto"
Tras esto, slo queda asignar el valor del nuevo atributo precedido del nuevo namespace,
por ejemplo con el texto Entrar:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:sgo="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity">
<net.sgoliver.android.controlpers2.ControlLogin
android:id="@+id/CtlLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#DDDDDD"
sgo:login_text="Entrar" />
</LinearLayout>
Con esto, conseguiramos el mismo efecto que los ejemplos antes mostrados, pero de una
forma mucho ms fcilmente personalizable, con el texto del botn controlado directamente
por un atributo del layout XML
Como resumen, en este artculo hemos visto cmo construir un control android
personalizado a partir de otros controles estandar, componiendo su interfaz, aadiendo
mtodos y eventos personalizados, e incluso aadiendo nuevas opciones en tiempo de
diseo aadiendo atributos xml exclusivos.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
Espero que os sea til y que sigis los artculos que quedan por venir.
Interfaz de usuario en Android: Controles personalizados (III)
112
En las anteriores ocasiones vimos cmo el nuevo control creado siempre heredaba de algn
otro control o contenedor ya existente. En este caso sin embargo, vamos a heredar nuestro
contro directamente de la clase View (clase padre de la gran mayora de elementos visuales
de Android). Esto implica, entre otras cosas, que por defecto nuestro control no va a tener
ningn tipo de interfaz grfica, por lo que todo el trabajo de dibujar la interfaz lo vamos a
tener que hacer nosotros. Adems, como paso previo a la representacin grfica de la
interfaz, tambin vamos a tener que determinar las dimensiones que nuestro control tendr
dentro de su elemento contenedor. Como veremos ahora, ambas cosas se llevarn a cabo
redefiniendo dos eventos de la clase View: onDraw() para el dibujo de la interfaz,
y onMeasure() para el clculo de las dimensiones.
Por llevar un orden cronolgico, empecemos comentando el evento onMeasure(). Este
evento se ejecuta automticamente cada vez que se necesita recalcular el tamao de un
control. Pero como ya hemos visto en varias ocasiones, los elementos grficos incluidos en
113
una aplicacin Android se distribuyen por la pantalla de una forma u otra dependiendo del
tipo de contenedor o layout utilizado. Por tanto, el tamao de un control determinado en la
pantalla no depender slo de l, sino de ciertas restricciones impuestas por su elemento
contenedor o elemento padre. Para resolver esto, en el evento onMeasure() recibiremos
como parmetros las restricciones del elemento padre en cuanto a ancho y alto del control,
con lo que podremos tenerlas en cuenta a la hora de determinar el ancho y alto de nuestro
control personalizado. Estas restricciones se reciben en forma de objetos MeasureSpec, que
contiene dos campos: modo ytamao. El significado del segundo de ellos es obvio, el
primero por su parte sirve para matizar el significado del segundo. Me explico. Este
campo modo puede contener tres valores posibles:
AT_MOST: indica que el control podr tener como mximo el tamao especificado.
EXACTLY: indica que al control se le dar exactamente el tamao especificado.
UNSPECIFIED: indica que el control padre no impone ninguna restriccin sobre el
tamao.
Dependiendo de esta pareja de datos, podremos calcular el tamao deseado para nuestro
control. Para nuestro control de ejemplo, apuraremos siempre el tamao mximo disponible
(o un tamao por defecto de 100*100 en caso de no recibir ninguna restriccin), por lo que
en todos los casos elegiremos como tamao de nuestro control el tamao recibido como
parmetro:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int ancho = calcularAncho(widthMeasureSpec);
int alto = calcularAlto(heightMeasureSpec);
if(ancho < alto)
alto = ancho;
else
ancho = alto;
setMeasuredDimension(ancho, alto);
}
private int calcularAlto(int limitesSpec)
{
int res = 100; //Alto por defecto
int modo = MeasureSpec.getMode(limitesSpec);
int limite = MeasureSpec.getSize(limitesSpec);
if (modo == MeasureSpec.AT_MOST) {
114
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
res = limite;
}
else if (modo == MeasureSpec.EXACTLY) {
res = limite;
}
return res;
}
private int calcularAncho(int limitesSpec)
{
int res = 100; //Ancho por defecto
int modo = MeasureSpec.getMode(limitesSpec);
int limite = MeasureSpec.getSize(limitesSpec);
if (modo == MeasureSpec.AT_MOST) {
res = limite;
}
else if (modo == MeasureSpec.EXACTLY) {
res = limite;
}
return res;
}
Como nota importante, al final del evento onMeasure() siempre debemos llamar al
mtodosetMeasuredDimension() pasando como parmetros el ancho y alto calculados para
nuestro control.
Con esto ya hemos determinado las dimensiones del control, por lo que tan slo nos queda
dibujar su interfaz grfica, pero antes vamos a ver qu datos nos har falta guardar para
poder almacenar el estado del control y, entre otras cosas, poder dibujar su interfaz
convenientemente.
Por un lado guardaremos en un array de 33 (tablero) el estado de cada casilla del tablero.
Cada casilla la rellenaremos con un valor constante dependiendo de si contiene una ficha X,
una ficha O, o si est vaca, valores para lo que definiremos tres constantes
(FICHA_X, FICHA_O, VACIA). Guardaremos tambin los colores que utilizarn las
fichas X y O (xColor y oColor), de forma que ms tarde podamos personalizarlos. Y por
ltimo, almacenaremos tambin la ficha activa, es decir, el tipo de ficha que se colocar al
pulsar sobre el tablero (fichaActiva).
1 public static final int VACIA = 0;
2 public static final int FICHA_O = 1;
115
116
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fichaActiva = ficha;
}
public int getFichaActiva() {
return fichaActiva;
}
public void alternarFichaActiva()
{
if(fichaActiva == FICHA_O)
fichaActiva = FICHA_X;
else
fichaActiva = FICHA_O;
}
public void setXColor(int color) { xColor = color; }
public int getXColor() { return xColor; }
public void setOColor(int color) { oColor = color; }
public int getOColor() { return oColor; }
public void setCasilla(int fil, int col, int valor) { tablero[fil][col] = valor; }
public int getCasilla(int fil, int col) {
return tablero[fil][col];
}
Pasemos ya al dibujo de la interfaz a partir de los datos anteriores. Como hemos indicado
antes, esta tarea se realiza dentro del evento onDraw(). Este evento recibe como parmetro
un objeto de tipo Canvas, sobre el que podremos ejecutar todas las operaciones de dibujo de
la interfaz. No voy a entrar en detalles de la clase Canvas, por ahora nos vamos a conformar
sabiendo que es la clase que contiene la mayor parte de los mtodos de dibujo en interfaces
Android,
por
ejemplo drawRect() para
dibujar
rectngulos,drawCircle() para
crculos, drawBitmap() para imagenes, drawText() para texto, e infinidad de posibilidades
ms. Para consultar todos los mtodos disponibles puedes dirigirte a la documentacin
oficial de la clase Canvas de Android. Adems de la clase Canvas, tambin me gustara
destacar la clase Paint, que permite definir el estilo de dibujo a utilizar en los metodos de
dibujo de Canvas, por ejemplo el ancho de trazado de las lneas, los colores de relleno, etc.
Para nuestro ejemplo no necesitaramos conocer nada ms, ya que la interfaz del control es
relativamente sencilla. Vemos primero el cdigo y despus comentamos los pasos
realizados:
117
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Override
protected void onDraw(Canvas canvas) {
//Obtenemos las dimensiones del control
int alto = getMeasuredHeight();
int ancho = getMeasuredWidth();
//Lineas
Paint pBorde = new Paint();
pBorde.setStyle(Style.STROKE);
pBorde.setColor(Color.BLACK);
pBorde.setStrokeWidth(2);
canvas.drawLine(ancho/3, 0, ancho/3, alto, pBorde);
canvas.drawLine(2*ancho/3, 0, 2*ancho/3, alto, pBorde);
canvas.drawLine(0, alto/3, ancho, alto/3, pBorde);
canvas.drawLine(0, 2*alto/3, ancho, 2*alto/3, pBorde);
//Marco
canvas.drawRect(0, 0, ancho, alto, pBorde);
//Marcas
Paint pMarcaO = new Paint();
pMarcaO.setStyle(Style.STROKE);
pMarcaO.setStrokeWidth(8);
pMarcaO.setColor(oColor);
Paint pMarcaX = new Paint();
pMarcaX.setStyle(Style.STROKE);
pMarcaX.setStrokeWidth(8);
pMarcaX.setColor(xColor);
//Casillas Seleccionadas
for(int fil=0; fil<3; fil++) {
for(int col=0; col<3; col++) {
if(tablero[fil][col] == FICHA_X) {
//Cruz
canvas.drawLine(
col * (ancho / 3) + (ancho / 3) * 0.1f,
fil * (alto / 3) + (alto / 3) * 0.1f,
col * (ancho / 3) + (ancho / 3) * 0.9f,
fil * (alto / 3) + (alto / 3) * 0.9f,
pMarcaX);
canvas.drawLine(
col * (ancho / 3) + (ancho / 3) * 0.1f,
118
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
119
ficha colocada. Para ello implementaremos el evento onTouch(), lanzado cada vez que el
usuario toca la pantalla sobre nuestro control. La lgica ser sencilla, simplemente
consultaremos las coordenadas donde ha pulsado el usuario (mediante los
mtodos getX() y getY()), y dependiendo del lugar pulsado determinaremos a qu casilla
del tablero se corresponde y actualizaremos el array tablero con el valor de la ficha
actual fichaActiva. Finalmente, llamamos al mtodoinvalidate() para refrescar la interfaz
del control:
1
@Override
2
public boolean onTouchEvent(MotionEvent event)
3
{
4
int fil = (int) (event.getY() / (getMeasuredHeight()/3));
5
int col = (int) (event.getX() / (getMeasuredWidth()/3));
6
7
//Actualizamos el tablero
8
tablero[fil][col] = fichaActiva;
9
10
//Refrescamos el control
11
this.invalidate();
12
13
return super.onTouchEvent(event);
14 }
En segundo lugar crearemos un evento externo personalizado, que lanzaremos cuando el
usuario
pulse
sobre
una
casilla
del
tablero.
Llamaremos
a
este
evento onCasillaSeleccionada(). Para crearlo actuaremos de la misma forma que ya vimos
en el artculo anterior. Primero definiremos una interfaz para el listener de nuestro evento:
1 package net.sgoliver.android.controlpers3;
2
3 public interface OnCasillaSeleccionadaListener
4 {
5
void onCasillaSeleccionada(int fila, int columna);
6 }
Posteriormente, definiremos un objeto de este tipo como atributo de nuestro control y
escribiremos un nuevo mtodo que permita a las aplicaciones suscribirse al evento:
1
2
3
4
5
6
7
8
9
120
10
}
11 }
Y ya slo nos quedara lanzar el evento en el momento preciso. Esto tambin lo haremos
dentro del eventoonTouch(), cuando detectemos que el usuario ha pulsado sobre una casilla
del tablero:
1
@Override
2
public boolean onTouchEvent(MotionEvent event)
3
{
4
int fil = (int) (event.getY() / (getMeasuredHeight()/3));
5
int col = (int) (event.getX() / (getMeasuredWidth()/3));
6
7
tablero[fil][col] = fichaActiva;
8
9
//Lanzamos el evento de pulsacin
10
if (listener != null) {
11
listener.onCasillaSeleccionada(fil, col);
12
}
13
14
//Refrescamos el control
15
this.invalidate();
16
17
return super.onTouchEvent(event);
18 }
Con esto, nuestra aplicacin principal ya podra suscribirse a este nuevo evento para estar
informada cada vez que se seleccione una casilla. En la aplicacin de ejemplo he incluido,
adems del tablero (terTablero), un botn para alternar la ficha activa (btnFicha), y una
etiqueta de texto para mostrar la casilla seleccionada (txtCasilla) haciendo uso de la
informacin recibida en el evento externoonCasillaSeleccionada():
1
public class MainActivity extends ActionBarActivity {
2
3
private Button btnFicha;
4
private TresEnRaya terTablero;
5
private TextView txtCasilla;
6
7
@Override
8
protected void onCreate(Bundle savedInstanceState) {
9
super.onCreate(savedInstanceState);
10
setContentView(R.layout.activity_main);
11
12
terTablero = (TresEnRaya)findViewById(R.id.tablero);
13
btnFicha = (Button)findViewById(R.id.btnFicha);
14
txtCasilla = (TextView)findViewById(R.id.txtCasilla);
15
16
btnFicha.setOnClickListener(new View.OnClickListener() {
121
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
public void onClick(View view) {
terTablero.alternarFichaActiva();
}
});
terTablero.setOnCasillaSeleccionadaListener(new OnCasillaSeleccionadaListener() {
@Override
public void onCasillaSeleccionada(int fila, int columna) {
txtCasilla.setText("ltima casilla seleccionada: " + fila + "." + columna);
}
});
}
//....
}
Por ltimo, al igual que en el apartado anterior, voy a definir dos atributos XML
personalizados para poder indicar los colores de las fichas X y O desde el propio layout
XML. Para ellos, creamos primero un nuevo fichero /res/values/attrs.xml :
1 <?xml version="1.0" encoding="utf-8"?>
2 <resources>
3
<declare-styleable name="TresEnRaya">
4
<attr name="ocolor" format="color"/>
5
<attr name="xcolor" format="color"/>
6
</declare-styleable>
7 </resources>
Posteriormente, accedemos a estos atributos desde nuestros constructores para establecer
con ellos los valores de las variables xColor y oColor, esto ya lo vimos en el apartado
anterior sobre controles compuestos:
1
public TresEnRaya(Context context) {
2
super(context);
3
4
inicializacion();
5
}
6
7
public TresEnRaya(Context context, AttributeSet attrs, int defaultStyle) {
8
super(context, attrs, defaultStyle );
9
10
inicializacion();
11
12
// Procesamos los atributos XML personalizados
13
TypedArray a =
14
getContext().obtainStyledAttributes(attrs,
15
R.styleable.TresEnRaya);
16
122
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
oColor = a.getColor(
R.styleable.TresEnRaya_ocolor, Color.BLUE);
xColor = a.getColor(
R.styleable.TresEnRaya_xcolor, Color.RED);
a.recycle();
}
public TresEnRaya(Context context, AttributeSet attrs) {
super(context, attrs);
inicializacion();
// Procesamos los atributos XML personalizados
TypedArray a =
getContext().obtainStyledAttributes(attrs,
R.styleable.TresEnRaya);
oColor = a.getColor(
R.styleable.TresEnRaya_ocolor, Color.BLUE);
xColor = a.getColor(
R.styleable.TresEnRaya_xcolor, Color.RED);
a.recycle();
}
Tras definir nuestros atributos, podemos ver cmo quedara el layout de nuestra actividad
principal haciendo uso de ellos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:sgo="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity">
<net.sgoliver.android.controlpers3.TresEnRaya
android:id="@+id/tablero"
android:layout_width="match_parent"
123
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
android:layout_height="match_parent"
android:layout_margin="10dp"
sgo:ocolor="#0000FF"
sgo:xcolor="#FF0000" />
<Button android:id="@+id/btnFicha"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Cambiar Ficha" />
<TextView android:id="@+id/txtCasilla"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp" />
</LinearLayout>
124
En artculos anteriores del Curso de Programacin Android hemos visto como dar forma a
la interfaz de usuario de nuestra aplicacin mediante el uso de diversos tipos de layouts,
como por ejemplo los lineales, absolutos, relativos, u otros ms elaborados como los de tipo
lista o tabla. stos van a ser siempre los elementos organizativos bsicos de nuestra
interfaz, pero sin embargo, dado el poco espacio con el que contamos en las pantallas de
muchos dispositivos, o simplemente por cuestiones de organizacin, a veces es
necesario/interesante dividir nuestros controles en varias pantallas. Y una de las formas
clsicas de conseguir esto consiste en la distribucin de los elementos por pestaas o tabs.
Android por supuesto permite utilizar este tipo de interfaces, aunque lo hace de una forma
un tanto peculiar, ya que la implementacin no va a depender de un slo elemento sino de
varios, que adems deben estar distribuidos y estructurados de una forma determinada nada
arbitraria. Adicionalmente no nos bastar simplemente con definir la interfaz en XML
como hemos hecho en otras ocasiones, sino que tambin necesitaremos completar el
conjunto con algunas lneas de cdigo. Desarrollemos esto poco a poco.
En Android, el elemento principal de un conjunto de pestaas ser el control TabHost. ste
va a ser el contenedor principal de nuestro conjunto de pestaas y deber tener
obligatoriamente como id el valor @android:id/tabhost. Dentro de ste vamos a incluir
un LinearLayout que nos servir para distribuir verticalmente las secciones principales del
layout: la seccin de pestaas en la parte superior y la seccin de contenido en la parte
inferior. La seccin de pestaas se representar mediante un elementoTabWidget, que
deber tener como id el valor @android:id/tabs, y como contenedor para el contenido de
las pestaas aadiremos un FrameLayout con el id obligatorio @android:id/tabcontent.
Por ltimo, dentro del FrameLayout incluiremos el contenido de cada pestaa,
normalmente cada uno dentro de su propio layout principal (en mi caso he
utilizado LinearLayout) y con un id nico que nos permita posteriormente hacer referencia
a ellos fcilmente (en mi caso he utilizado por ejemplo los ids tab1, tab2, ). A
continuacin represento de forma grfica toda la estructura descrita.
125
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent">
<TabHost android:id="@android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TabWidget android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@android:id/tabs" />
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@android:id/tabcontent" >
<LinearLayout android:id="@+id/tab1"
android:orientation="vertical"
android:layout_width="match_parent"
126
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
android:layout_height="match_parent" >
<TextView android:id="@+id/textView1"
android:text="Contenido Tab 1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout android:id="@+id/tab2"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView android:id="@+id/textView2"
android:text="Contenido Tab 2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</TabHost>
</LinearLayout>
Como podis ver, como contenido de las pestaas tan slo he aadido por simplicidad una
etiqueta de texto con el texto Contenido Tab NTab. Esto nos permitir ver que el
conjunto de pestaas funciona correctamente cuando ejecutemos la aplicacin.
Con esto ya tendramos montada toda la estructura de controles necesaria para nuestra
interfaz de pestaas. Sin embargo, como ya dijimos al principio del artculo, con esto no es
suficiente. Necesitamos asociar de alguna forma cada pestaa con su contenido, de forma
que el el control se comporte correctamente cuando cambiamos de pestaa. Y esto
tendremos que hacerlo mediante cdigo en nuestra actividad principal.
Empezaremos obteniendo una referencia al control principal TabHost y preparndolo para
su configuracin llamando a su mtodo setup(). Tras esto, crearemos un objeto de
tipo TabSpec para cada una de las pestaas que queramos aadir mediante el
mtodo newTabSpec(), al que pasaremos como parmetro una etiqueta identificativa de la
pestaa (en mi caso de ejemplo mitab1, mitab2, ). Adems, tambin le asignaremos
el layout de contenido correspondiente a la pestaa llamando al mtodo setContent(), e
indicaremos el texto y el icono que queremos mostrar en la pestaa mediante el
mtodosetIndicator(texto, icono). Veamos el cdigo completo.
1
Resources res = getResources();
2
3
TabHost tabs=(TabHost)findViewById(android.R.id.tabhost);
127
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tabs.setup();
TabHost.TabSpec spec=tabs.newTabSpec("mitab1");
spec.setContent(R.id.tab1);
spec.setIndicator("",
res.getDrawable(android.R.drawable.ic_btn_speak_now));
tabs.addTab(spec);
spec=tabs.newTabSpec("mitab2");
spec.setContent(R.id.tab2);
spec.setIndicator("TAB2",
res.getDrawable(android.R.drawable.ic_dialog_map));
tabs.addTab(spec);
tabs.setCurrentTab(0);
Si vemos el cdigo, vemos por ejemplo como para la primera pestaa creamos un
objeto TabSpec con la etiqueta mitab1, le asignamos como contenido uno de
los LinearLayout que incluimos en la seccin de contenido (en este caso R.id.tab1) y
finalmente
le
asignamos
el
texto
TAB1
y
el
iconoandroid.R.drawable.ic_btn_speak_now (ste es un icono incluido con la propia
plataforma Android. Si no existiera en vuestra versin podis sustituirlo por cualquier otro
icono). Finalmente aadimos la nueva pestaa al control mediante el mtodo addTab().
Si ejecutamos ahora la aplicacin tendremos algo como lo que muestra la siguiente imagen,
donde podremos cambiar de pestaa y comprobar cmo se muestra correctamente el
contenido de la misma.
En Android 2.x
Y en Android 4.x
128
Una pequea sorpresa. Como podis comprobar, aunque estamos indicando en todos los
casos un texto y un icono para cada pestaa, el comportamiento difiere entre las distintas
versiones de Android. En Android 4, el comportamiento por defecto del control TabHost es
mostrar slo el texto, o solo el icono, pero no ambos. Si eliminamos el texto de la primera
pestaa y volvemos a ejecutar veremos como el icono s aparece.
1 TabHost.TabSpec spec=tabs.newTabSpec("mitab1");
2 spec.setContent(R.id.tab1);
3 spec.setIndicator("",
4
res.getDrawable(android.R.drawable.ic_btn_speak_now));
5 tabs.addTab(spec);
Con esta pequea modificacin la aplicacin se vera as en Android 4.x
En cuanto a los eventos disponibles del control TabHost, aunque no suele ser necesario
capturarlos, podemos ver a modo de ejemplo el ms interesante de ellos, OnTabChanged,
que se lanza cada vez que se cambia de pestaa y que nos informa de la nueva pestaa
visualizada. Este evento podemos implementarlo y asignarlo mediante el
mtodo setOnTabChangedListener() de la siguiente forma:
1 tabs.setOnTabChangedListener(new OnTabChangeListener() {
2
@Override
3
public void onTabChanged(String tabId) {
4
Log.i("AndroidTabsDemo", "Pulsada pestaa: " + tabId);
5
}
6 });
En el mtodo onTabChanged() recibimos como parmetro la etiqueta identificativa de la
pestaa
(no
su
ID),
que
debimos
asignar
cuando
creamos
su
129
objeto TabSpec correspondiente. Para este ejemplo, lo nico que haremos al detectar un
cambio de pestaa ser escribir en el log de la aplicacin un mensaje informativo con la
etiqueta de la nueva pestaa visualizada. As por ejemplo, al cambiar a la segunda pestaa
recibiremos el mensaje de log: Pulsada pestaa: mitab2.
Interfaz de usuario en Android: Fragments
by Sgoliver on 25/01/2013 in Android, Programacin
Cuando empezaron a aparecer dispositivos de gran tamao tipo tablet, el equipo de Android
tuvo que solucionar el problema de la adaptacin de la interfaz grfica de las aplicaciones a
ese nuevo tipo de pantallas. Una interfaz de usuario diseada para un telfono mvil no se
adaptaba fcilmente a una pantalla 4 o 5 pulgadas mayor. La solucin a esto vino en forma
de un nuevo tipo de componente llamado Fragment.
Un fragment no puede considerarse ni un control ni un contenedor, aunque se parecera
ms a lo segundo. Un fragment podra definirse como una porcin de la interfaz de usuario
que puede aadirse o eliminarse de una interfaz de forma independiente al resto de
elementos de la actividad, y que por supuesto puede reutilizarse en otras actividades. Esto,
aunque en principio puede parecer algo trivial, nos va a permitir poder dividir nuestra
interfaz en varias porciones de forma que podamos disear diversas configuraciones de
pantalla, dependiendo de su tamao y orientacin, sin tener que duplicar cdigo en ningn
momento, sino tan slo utilizando o no los distintos fragmentos para cada una de las
posibles configuraciones. Intentemos aclarar esto un poco con un ejemplo.
No quera utilizar el ejemplo tpico que aparece por todos lados, pero en este caso creo que
es el ms ilustrativo. Supongamos una aplicacin de correo electrnico, en la que por un
lado debemos mostrar la lista de correos disponibles, con sus campos clsicos De y Asunto,
y por otro lado debemos mostrar el contenido completo del correo seleccionado. En un
telfono mvil lo habitual ser tener una primera actividad que muestre el listado de
correos, y cuando el usuario seleccione uno de ellos navegar a una nueva actividad que
muestre el contenido de dicho correo. Sin embargo, en un tablet puede existir espacio
suficiente para tener ambas partes de la interfaz en la misma pantalla, por ejemplo en un
tablet en posicin horizontal podramos tener una columna a la izquierda con el listado de
correos y dedicar la zona derecha a mostrar el detalle del correo seleccionado, todo ello sin
tener que cambiar de actividad.
Antes de existir los fragments podramos haber hecho esto implementando diferentes
actividades con diferentes layouts para cada configuracin de pantalla, pero esto nos habra
130
obligado a duplicar gran parte del cdigo en cada actividad. Tras la aparicin de los
fragments, colocaramos el listado de correos en un fragment y la vista de detalle en otro,
cada uno de ellos acompaado de su lgica de negocio asociada, y tan slo nos quedara
definir varios layouts para cada configuracin de pantalla incluyendo [o no] cada uno de
estos fragments.
A modo de aplicacin de ejemplo para este artculo, nosotros vamos a simular la aplicacin
de correo que hemos comentado antes, adaptndola a tres configuraciones distintas:
pantalla normal, pantalla grande horizontal y pantalla grande vertical. Para el primer caso
colocaremos el listado de correos en una actividad y el detalle en otra, mientras que para el
segundo y el tercero ambos elementos estarn en la misma actividad, a derecha/izquierda
para el caso horizontal, y arriba/abajo en el caso vertical.
Definiremos por tanto dos fragments: uno para el listado y otro para la vista de detalles.
Ambos sern muy sencillos. Al igual que una actividad, cada fragment se compondr de un
fichero de layout XML para la interfaz (colocado en alguna carpeta /res/layout) y una clase
java para la lgica asociada.
El primero de los fragment a definir contendr tan slo un control ListView, para el que
definiremos un adaptador personalizado para mostrar dos campos por fila (De y
Asunto). Ya describimos cmo hacer esto en el artculo dedicado al control ListView. El
layout XML (lo llamaremos fragment_listado.xml) quedara por tanto de la siguiente
forma:
1
<?xml version="1.0" encoding="utf-8"?>
2
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
android:layout_width="match_parent"
4
android:layout_height="match_parent"
5
android:orientation="vertical" >
6
<ListView
7
android:id="@+id/LstListado"
8
android:layout_width="match_parent"
9
android:layout_height="wrap_content" >
10
</ListView>
11
</LinearLayout>
Como hemos dicho, todo fragment debe tener asociada, adems del layout, su propia clase
java, que en este caso debe extender de la clase Fragment. Y aqu viene el primer
contratiempo. Los fragment aparecieron con la versin 3 de Android, por lo que en
principio no estaran disponibles para versiones anteriores. Sin embargo, Google pens en
todos y proporcion esta caracterstica tambin como parte de la librera de
131
Hecho esto, ya no habra ningn problema para utilizar la clase Fragment, y otras que
comentaremos ms adelante, para utilizar fragmentos compatibles con la mayora de
versiones de Android. Veamos cmo quedara nuestra clase asociada al fragment de listado.
1
package net.sgoliver.android.fragments;
2
3
import android.app.Activity;
4
import android.os.Bundle;
5
import android.support.v4.app.Fragment;
6
import android.view.LayoutInflater;
7
import android.view.View;
8
import android.view.ViewGroup;
9
import android.widget.AdapterView;
10
import android.widget.ArrayAdapter;
11
import android.widget.ListView;
12
import android.widget.TextView;
13
14
public class FragmentListado extends Fragment {
132
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
133
62
63
64
65
return(item);
}
}
}
La clase Correo es una clase sencilla, que almacena los campos De, Asunto y Texto de un
correo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package net.sgoliver.android.fragments;
public class Correo
{
private String de;
private String asunto;
private String texto;
public Correo(String de, String asunto, String texto){
this.de = de;
this.asunto = asunto;
this.texto = texto;
}
public String getDe(){
return de;
}
public String getAsunto(){
return asunto;
}
public String getTexto(){
return texto;
}
}
Si observamos con detenimiento las clases anteriores veremos que no existe casi ninguna
diferencia con los temas ya comentados en artculos anteriores del curso sobre utilizacin
de controles de tipo lista y adaptadores personalizados. La nica diferencia que
encontramos aqu respecto a ejemplos anteriores, donde definamos actividades en vez de
fragments, son los mtodos que sobrescribimos. En el caso de los fragment son
normalmente dos: onCreateView() y onActivityCreated().
El primero de ellos, onCreateView(), es el equivalente al onCreate() de las actividades, es
decir, que dentro de l es donde normalmente asignaremos un layout determinado al
fragment. En este caso tendremos que inflarlo mediante el mtodo inflate() pasndole
como parmetro el ID del layout correspondiente, en nuestro caso fragment_listado.
134
que
135
19
20
public void mostrarDetalle(String texto) {
21
TextView txtDetalle =
22
(TextView)getView().findViewById(R.id.TxtDetalle);
23
24
txtDetalle.setText(texto);
25
}
26
}
Una vez definidos los dos fragments, ya tan solo nos queda definir las actividades de
nuestra aplicacin, con sus respectivos layouts que harn uso de los fragments que
acabamos de implementar.
Para la actividad principal definiremos 3 layouts diferentes: el primero de ellos para los
casos en los que la aplicacin se ejecute en una pantalla normal (por ejemplo un telfono
mvil) y dos para pantallas grandes (uno pensado para orientacin horizontal y otro para
vertical). Todos se llamar activity_main.xml, y lo que marcar la diferencia ser la carpeta
en la que colocamos cada uno. As, el primero de ellos lo colocaremos en la carpeta por
defecto /res/layout, y los otros dos en las carpetas /res/layout-large(pantalla grande)
y /res/latout-large-port (pantalla grande con orientacin vertical) respectivamente. De esta
forma, segn el tamao y orientacin de la pantalla Android utilizar un layout u otro de
forma automtica sin que nosotros tengamos que hacer nada.
Para el caso de pantalla normal, la actividad principal mostrar tan slo el listado de
correos, por lo que el layout incluir tan slo el fragment FragmentListado.
1
<?xml version="1.0" encoding="utf-8"?>
2
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
3
class="net.sgoliver.android.fragments.FragmentListado"
4
android:id="@+id/FrgListado"
5
android:layout_width="match_parent"
6
android:layout_height="match_parent" />
Como podis ver, para incluir un fragment en un layout utilizaremos una
etiqueta <fragment> con un atributo class que indique la ruta completa de la clase java
correspondiente
al
fragment,
en
este
primer
caso
net.sgoliver.android.fragments.FragmentListado. Los dems atributos utilizados son los
que ya conocemos de id, layout_width y layout_height.
En este caso de pantalla normal, la vista de detalle se mostrar en una segunda actividad,
por lo que tambin tendremos que crear su layout, que llamaremos activity_detalle.xml.
Veamos rpidamente su implementacin:
1
<?xml version="1.0" encoding="utf-8"?>
2
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
3
class="net.sgoliver.android.fragments.FragmentDetalle"
136
4
5
6
android:id="@+id/FrgDetalle"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Como vemos es anloga a la anterior, con la nica diferencia de que aadimos el fragment
de detalle en vez de el de listado.
Por su parte, el layout para el caso de pantalla grande horizontal, ser de la siguiente forma:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Como veis en este caso incluimos los dos fragment en la misma pantalla, ya que tendremos
espacio de sobra, ambos dentro de un LinearLayout horizontal, asignando al primero de
ellos un peso (propiedadlayout_weight) de 30 y al segundo de 70 para que la columna de
listado ocupe un 30% de la pantalla a la izquierda y la de detalle ocupe el resto.
Por ltimo, para el caso de pantalla grande vertical ser practicamente igual, slo que
usaremos unLinearLayout vertical.
1
<?xml version="1.0" encoding="utf-8"?>
2
<LinearLayout
3
xmlns:android="http://schemas.android.com/apk/res/android"
4
android:orientation="vertical"
5
android:layout_width="match_parent"
6
android:layout_height="match_parent">
7
8
<fragment class="net.sgoliver.android.fragments.FragmentListado"
9
android:id="@+id/FrgListado"
10
android:layout_weight="40"
137
11
12
13
14
15
16
17
18
19
20
android:layout_width="match_parent"
android:layout_height="0px" />
<fragment class="net.sgoliver.android.fragments.FragmentDetalle"
android:id="@+id/FrgDetalle"
android:layout_weight="60"
android:layout_width="match_parent"
android:layout_height="0px" />
</LinearLayout>
138
139
140
33
34
35
this.listener=listener;
}
}
Como vemos, una vez definida toda esta parafernalia, lo nico que deberemos hacer en el
eventoonItemClick() de
la
lista
ser
lanzar
nuestro
evento
personalizado onCorreoSeleccionado()pasndole como parmetro el contenido del correo,
que en este caso obtendremos accediendo al adaptador con getAdapter() y recuperando el
elemento con getItem().
Hecho esto, el siguiente paso ser tratar este evento en la clase java de nuestra actividad
principal. Para ello, en el onCreate() de nuestra actividad, obtendremos una referencia al
fragment mediante el mtodogetFragmentById() del fragment manager (componente
encargado de gestionar los fragments) y asignaremos el evento mediante su
mtodo setCorreosListener() que acabamos de definir.
1
package net.sgoliver.android.fragments;
2
3
import net.sgoliver.android.fragments.FragmentListado.CorreosListener;
4
import android.content.Intent;
5
import android.os.Bundle;
6
import android.view.Menu;
7
import android.support.v4.app.FragmentActivity;
8
9
public class MainActivity extends FragmentActivity implements CorreosListener {
10
11
@Override
12
protected void onCreate(Bundle savedInstanceState) {
13
super.onCreate(savedInstanceState);
14
setContentView(R.layout.activity_main);
15
16
FragmentListado frgListado
17
=(FragmentListado)getSupportFragmentManager()
18
.findFragmentById(R.id.FrgListado);
19
20
frgListado.setCorreosListener(this);
21
}
22
23
@Override
24
public void onCorreoSeleccionado(Correo c) {
25
boolean hayDetalle =
26
(getSupportFragmentManager().findFragmentById(R.id.FrgDetalle) != null);
27
28
if(hayDetalle) {
29
((FragmentDetalle)getSupportFragmentManager()
30
.findFragmentById(R.id.FrgDetalle)).mostrarDetalle(c.getTexto());
31
}
141
32
33
34
35
36
37
38
else {
Intent i = new Intent(this, DetalleActivity.class);
i.putExtra(DetalleActivity.EXTRA_TEXTO, c.getTexto());
startActivity(i);
}
}
}
Como vemos en el cdigo anterior, en este caso hemos hecho que nuestra actividad herede
de nuestra interfaz CorreosListener, por lo que nos basta pasar this al
mtodo setCorreosListener(). Adicionalmente, un detalle importante a descatar es que la
actividad no hereda de la clase Activity como de costumbre, sino de FragmentActivity.
Esto es as porque estamos utilizando la librera de compatibilidad android-support para
utilizar fragments conservando la compatibilidad con versiones de Android anteriores a la
3.0. En caso de no necesitar esta compatibilidad podras seguir heredando deActivity sin
problemas.
La mayor parte del inters de la clase anterior est en el mtodo onCorreoSeleccionado().
ste es el mtodo que se ejecutar cuando el fragment de listado nos avise de que se ha
seleccionado un determinado item de la lista. Esta vez s, la lgica ser la ya mencionada,
es decir, si en la pantalla existe el fragment de detalle simplemente lo actualizaremos
mediante mostrarDetalle() y en caso contrario navegaremos a la actividad DetalleActivity.
Para este segundo caso, crearemos un nuevo Intent con la referencia a dicha clase, y le
aadiremos como parmetro extra un campo de texto con el contenido del correo
seleccionado. Finalmente llamamos a startActivity() para iniciar la nueva actividad.
Y ya slo nos queda comentar la implementacin de esta segunda
actividad, DetalleActivity. El cdigo ser muy sencillo, y se limitar a recuperar el
parmetro extra pasado desde la actividad anterior y mostrarlo en el fragment de detalle
mediante su mtodo mostrarDetalle(), todo ello dentro de su mtodo onCreate().
1
package net.sgoliver.android.fragments;
2
3
import android.os.Bundle;
4
import android.support.v4.app.FragmentActivity;
5
6
public class DetalleActivity extends FragmentActivity {
7
8
public static final String EXTRA_TEXTO =
9
"net.sgoliver.android.fragments.EXTRA_TEXTO";
10
11
@Override
12
protected void onCreate(Bundle savedInstanceState) {
13
super.onCreate(savedInstanceState);
14
setContentView(R.layout.activity_detalle);
142
15
16
17
18
19
20
21
22
23
FragmentDetalle detalle =
(FragmentDetalle)getSupportFragmentManager()
.findFragmentById(R.id.FrgDetalle);
detalle.mostrarDetalle(
getIntent().getStringExtra(EXTRA_TEXTO));
}
}
La action bar de Android es uno de esos componentes que Google no ha tratado demasiado
bien al no incluirla en la librera de compatibilidad android-support. Esto significa que de
forma nativa tan slo es compatible con versiones de Android 3.0 o superiores. En este
artculo nos centraremos nicamente en esta versin nativa de la plataforma
(configuraremos nuestra aplicacin de ejemplo para ejecutarse sobre Android 4).
Tan slo a modo de referencia dir que existe una librera muy popular
llamada ActionBarSherlock que proporciona una implementacin alternativa de este
componente que es compatible con versines de Android a partir de la 2.0. Otra alternativa
para compatibilizar nuestras aplicaciones con versiones de Android anteriores a la 3.0 sera
utilizar la implementacin que Google proporciona como parte de los ejemplos de la
plataforma, llamada ActionBarCompat, que puedes encontrar en la siguiente carpeta de tu
instalacin del SDK: <carpeta-sdk>\samples\android-nn\ActionBarCompat
143
Cuando se crea un proyecto con alguna de las ltimas versiones del plugin ADT para
Eclipse, la aplicacin creada por defecto ya incluye de serie su action bar
correspondiente. De hecho, si creamos un proyecto nuevo y directamente lo ejecutamos
sobre un AVD con Android 3.0 o superior veremos como no solo se incluye la action bar
sino que tambin aparece una accin Settings como muestra la siguiente imagen.
Vale, pero donde est todo esto definido en nuestro proyecto? La action bar de Android
toma su contenido de varios sitios diferentes. En primer lugar muestra el icono de la
aplicacin (definido en elAndroidManifest mediante el atributo android:icon del
elemento <application>) y el ttulo de la actividad actual (definido en el AndroidManifest
mediante el atributo android:label de cada elemento<activity>). El resto de elementos se
definen de la misma forma que los antiguos mens de aplicacin que se utilizaban en
versiones de Android 2.x e inferiores. De hecho, es exactamente la misma implementacin.
Nosotros definiremos un men, y si la aplicacin se ejecuta sobre Android 2.x las acciones
se mostrarn como elementos del men como tal (ya que la action bar no se ver al no ser
compatible) y si se ejecuta sobre Android 3.0 o superior aparecern como acciones de la
action bar, ya sea en forma de botn de accin o incluidas en el men de overflow. En
definitiva, una bonita forma de mantener cierta compatibilidad con versiones anteriores de
Android, aunque en unas ocasiones se muestre la action bar y en otras no.
Y bien, cmo se define un men de aplicacin? Pues en el curso hay un artculo dedicado
exclusivamente a ello donde poder profundizar, pero de cualquier forma, en este artculo
dar las directrices generales para definir uno sin mucha dificultad.
144
o
o
145
18
android:title="@string/menu_nuevo"/>
19 </menu>
Como podis ver en la segunda opcin, se pueden combinar varios valores
de showAsAction utilizando el caracter |.
Una vez definido el men en su fichero XML correspondiente tan slo queda asociarlo a
nuestra
actividad
principal.
Esto
se
realiza
sobrescribiendo
el
mtodo OnCreateOptionsMenu() de la actividad, dentro del cual lo nico que tenemos que
hacer normalmente es inflar el men llamando al mtodo inflate()pasndole como
parmetros el ID del fichero XML donde se ha definido. Este trabajo suele venir hecho ya
al crear un proyecto nuevo desde Eclipse:
1
public class MainActivity extends Activity {
2
3
...
4
5
@Override
6
public boolean onCreateOptionsMenu(Menu menu) {
7
// Inflate the menu; this adds items to the action bar if it is present.
8
getMenuInflater().inflate(R.menu.activity_main, menu);
9
return true;
10
}
11 }
Ejecutemos la aplicacin ahora a ver qu ocurre.
Como podemos observar, la opcin Settings sigue estando dentro del men de overflow,
y ahora aparecen como botones de accin las dos opciones que hemos marcado
como showAsAction=ifRoom, pero para la segunda no aparece el texto. Y por qu?
Porque no hay espacio disponible con la pantalla en vertical. Pero si rotamos el emulador
para ver qu ocurre con la pantalla en horizontal (pulsando Ctrl + F12) vemos lo siguiente:
146
147
Android detecta que hay suficiente espacio disponible en la action bar, integrar las
pestaas dentro de la propia action bar de forma que no ocupen espacio extra en la pantalla.
Si por el contrario no hubiera espacio suficiente colocara las pestaas bajo la action bar
como de costumbre. Ms tarde veremos un ejemplo grfico de esto, pero ahora empecemos.
Lo primero que debemos hacer ser crear un nuevo fragment para albergar el contenido de
cada una de las pestaas que tendr nuestra aplicacin. Como ya vimos en el artculo
dedicado a los fragments, necesitaremos crear como mnimo una pareja de ficheros para
cada fragment, el primero para el layout XML y el segundo para su cdigo java asociado.
Para el caso de ejemplo de este artculo, que partir del ya construido en el artculo anterior,
incluir tan slo dos pestaas, y los ficheros asociados a ellas los llamar de la siguiente
forma:
Pestaa 1:
Tab1Fragment.java
fragment1.xml
Pestaa 2:
Tab2Fragment.java
fragment2.xml
La interfaz de los fragmets ser mnima para no complicar el ejemplo, y contendrn
nicamente una etiqueta de texto Tab 1 o Tab 2 para poder diferenciarlas. Por ejemplo
para la pestaa 1 tendramos un fichero fragment1.xml con el siguiente contenido:
1
<?xml version="1.0" encoding="utf-8"?>
2
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
android:layout_width="match_parent"
4
android:layout_height="match_parent"
5
android:orientation="vertical" >
6
7
<TextView
8
android:id="@+id/textView1"
9
android:layout_width="wrap_content"
10
android:layout_height="wrap_content"
11
android:text="@string/tab_1" />
12
13 </LinearLayout>
Por su parte, su clase java asociada Tab1Fragment.java no tendr ninguna funcionalidad,
por lo que el cdigo se limita a inflar el layout y poco ms:
1
package net.sgoliver.android.actionbartabs;
2
3
import android.app.Fragment;
4
import android.os.Bundle;
5
import android.view.LayoutInflater;
6
import android.view.View;
148
7
8
9
10
11
12
13
14
15
import android.view.ViewGroup;
public class Tab1Fragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment1, container, false);
}
}
149
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Como podis ver, en cada uno de estos mtodos lo nico que haremos en nuestro caso ser
mostrar u ocultar nuestros fragments de forma que quede visible el correspondiente a la
pestaa seleccionada. As, en el evento onTabSelected() reemplazaremos el fragment
actualmente visible con el de la pestaa seleccionada (que ser un atributo de nuestra clase,
despus veremos dnde y cmo se asigna), y en el mtodo onTabUnselected() ocultamos el
fragment asociado a la pestaa ya que est habr sido deseleccionada. Ambas acciones se
realizan llamando al mtodo correspondiente deFragmentTransaction, que nos llega
siempre como parmetro y nos permite gestionar los fragments de la actividad. En el primer
caso se usar el mtodo replace() y en el segundo el mtodo remove(). Adems de todo
esto, he aadido mensajes de log en cada caso para poder comprobar que se lanzan los
eventos de forma correcta.
Implementado el listener tan slo nos queda crear las pestaas, asociarle sus fragments
correspondientes, y enlazarlas a nuestra action bar. Todo esto lo haremos en el
mtodo onCreate() de la actividad principal. Son muchos pasos, pero sencillos todos ellos.
Comenzaremos obteniendo una referencia a la action bar mediante el
mtodo getActionBar(). Tras esto estableceremos su mtodo de navegacin
a NAVIGATION_MODE_TABS para que sepa que debe mostrar las pestaas. A
continuaci creamos las pestaas el mtodo newTab() de la action bar y estableceremos su
texto con setText(). Lo siguiente ser instanciar los dos fragments y asociarlos a cada
pestaa a travs de la clase listener que hemos creado, llamando para ello
a setTabListener(). Y por ltimo aadiremos las pestaas a la action bar mediante addTab().
Estoy seguro que con el cdigo se entender mucho mejor:
1
@Override
2
protected void onCreate(Bundle savedInstanceState) {
3
super.onCreate(savedInstanceState);
150
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
setContentView(R.layout.activity_main);
//Obtenemos una referencia a la actionbar
ActionBar abar = getActionBar();
//Establecemos el modo de navegacin por pestaas
abar.setNavigationMode(
ActionBar.NAVIGATION_MODE_TABS);
//Ocultamos si queremos el ttulo de la actividad
//abar.setDisplayShowTitleEnabled(false);
//Creamos las pestaas
ActionBar.Tab tab1 =
abar.newTab().setText("Tab 1");
ActionBar.Tab tab2 =
abar.newTab().setText("Tab 2");
//Creamos los fragments de cada pestaa
Fragment tab1frag = new Tab1Fragment();
Fragment tab2frag = new Tab2Fragment();
//Asociamos los listener a las pestaas
tab1.setTabListener(new MiTabListener(tab1frag));
tab2.setTabListener(new MiTabListener(tab2frag));
//Aadimos las pestaas a la action bar
abar.addTab(tab1);
abar.addTab(tab2);
}
Como vemos, Android ha colocado nuestras pestaas bajo la action bar porque no ha
encontrado sitio suficiente arriba. Pero si probamos con la orientacin horizontal
(Ctrl+F12) pasa lo siguiente (click para ampliar):
151
Dado que ya haba espacio suficiente en la propia action bar, Android ha colocado las
pestaas directamente sobre ella de forma que hemos ganado espacio para el resto de la
interfaz. Esto no lo habramos podido conseguir utilizando el control TabWidget.
Por ltimo, si cambiamos varias veces de pestaa deberamos comprobar que se muestra en
cada caso el fragment correcto y que el en log aparecen los mensajes de depuracin que
hemos incluido.
Espero que con estos dos ltimos artculos hayis aprendido al menos a aprovechar la
funcionalidad bsica de la action bar. Habra por supuesto ms cosas que contar, por
ejemplo algunas cuestiones de navegacin entre actividades, pero eso lo dejaremos para
ms adelante.
Action Bar en Android (III): ActionBar Compat
by Admin on 03/08/2013 in Android, Programacin
Hace ya algn tiempo que hablamos en el curso sobre uno de los elementos visuales ms
representativos de las aplicaciones Android actuales, la Action Bar. Por aquel entonces
coment que Google haba maltratado un poco a este componente al no incluirlo en la
librera de compatibilidad android-support, lo que haca que no fuera compatible con
versiones de Android anteriores a la 3.0. Esto obligaba a recurrir a libreras externas
como ActioBarSherlock para hacer nuestra aplicacin compatible con cualquier versin de
Android.
Y hemos tenido que esperar hasta hace unos das (julio de 2013) para ver por fin a la Action
Bar debutar, de serie, en la librera de compatibilidad de Android. A partir de la revisin 18
de esta librera contamos con una versin de la Action Bar (conocida tambin
como ActionBarCompat) compatible con cualquier versin Android a partir de la 2.1 (API
7). Y es de esto de lo que nos vamos a ocupar en este nuevo artculo, ya que aunque su uso
es muy similar a lo que ya comentamos en el primer artculo sobre la Action Bar, son
necesarios algunos pequeos cambios en el cdigo y en la configuracin del proyecto que
justifican una nueva entrada para este tema.
Lo primero que tendremos que asegurar es que tenemos instalada la ltima versin de la
librera de compatibilidad (Android Support Library). Para ello entramos al SDK Manager
y actualizamos la librera a la ltima revisin (la 18 en el momento de escribir este
artculo).
152
153
154
Ms temas a tener en cuenta. Tendremos que sustituir (o extender) el tema visual utilizado
en la aplicacin por alguno de los definidos especficamente para la librera de
compatibilidad:
Theme.AppCompat
Theme.AppCompat.Light
Theme.AppCompat.Light.DarkActionBar
En mi caso voy a utilizar el ltimo de ellos. Modificaremos para ello nuestro fichero
AndroidManifest.xml indicando el nuevo tema a utilizar en el atributo android:theme del
elemento <application> (aunque tambin se puede definir a nivel de actividades).
1
...
2
<application
3
android:allowBackup="true"
4
android:icon="@drawable/ic_launcher"
5
android:label="@string/app_name"
6
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" >
7
<activity
8
android:name="net.sgoliver.android.abcompat.MainActivity"
9
android:label="@string/app_name" >
10
<intent-filter>
11
<action android:name="android.intent.action.MAIN" />
12
<category android:name="android.intent.category.LAUNCHER" />
13
</intent-filter>
14
</activity>
15
</application>
16 ...
155
156
como botn de accin si hay espacio para ella. De esta forma, la definicin completa de mi
men quedara de la siguiente forma:
1
<menu xmlns:android="http://schemas.android.com/apk/res/android"
2
xmlns:sgoliver="http://schemas.android.com/apk/res-auto" >
3
4
<item
5
android:id="@+id/action_settings"
6
android:orderInCategory="100"
7
sgoliver:showAsAction="never"
8
android:title="@string/action_settings"/>
9
10
<item
11
android:id="@+id/action_search"
12
android:orderInCategory="100"
13
sgoliver:showAsAction="ifRoom"
14
android:icon="@drawable/ic_action_search"
15
android:title="@string/action_search"/>
16 </menu>
Como puede verse en el cdigo anterior, al margen de la definicin del nuevo espacio de
nombres y de la utilizacin del atributo showAsAction asociado a l, el resto del cdigo es
exactamente igual que el que ya vimos en el artculo original sobre la action bar.
Por ltimo, para responder a las pulsaciones sobre las distintas acciones que hemos incluido
en la action bar podemos seguir utilizando el evento onOptionsItemSelected, en el que
dependiendo del ID de la opcin pulsada ejecutaremos las acciones que corresponda. En mi
caso voy simplemente a mostrar un toast con la opcin seleccionada:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId())
{
case R.id.action_settings:
Toast.makeText(this, "Settings", Toast.LENGTH_SHORT).show();;
break;
case R.id.action_search:
Toast.makeText(this, "Search", Toast.LENGTH_SHORT).show();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
157
Llegados a este punto ya podemos ejecutar nuestro proyecto en el emulador para ver que
todo funciona correctamente. En primer lugar lo vamos a hacer sobre un AVD con Android
4.2. Podemos ver cmo no existe ninguna diferencia, ni en aspecto ni en comportamiento,
con la action bar tradicional.
Todo parece correcto, salvo porque no aparece el men de overflow dnde estn las
opciones que hemos configurado para que aparezcan en el men de la action bar? Pues
siguen presentes, pero no en el mismo lugar. Android establece que si el dispositivo tiene
un botn fsico de Men, el botn de overflow desaparece y se sustituye por un men
tradicional que aparece en la zona inferior cuando se pulsa el botn Men del dispositivo.
Este ser el caso de la mayora de dispositivos que funcionen con Android 2.x. Si pulsamos
el botn men del emulador veremos como ahora s aparece nuestra accin Settings.
158
Y hasta aqu el artculo. Espero que hayan quedado claros los pasos necesarios para utilizar
esta nueva versin de la Action Bar compatible con la mayora de verisiones de Android.
Todo sea por que ms personas puedan disfrutar de nuestras aplicaciones en su totalidad.
Interfaz de Usuario en Android: Navigation Drawer
by Admin on 10/08/2013 in Android, Programacin
En el artculo anterior hablamos sobre una de las ltimas novedades del SDK de Android,
la nueva versin de la Action Bar incluida en la librera de compatibilidad android-support.
En este nuevo artculo vamos a tratar otra de las novedades presentadas en el Google I/O de
este ao relacionadas la capa de presentacin de nuestras aplicaciones, el men lateral
deslizante, o dicho de una forma mucho ms moderna y elegante: el Navigation Drawer.
Este tipo de mens de navegacin ya llevaban tiempo utilizndose y proliferando por el
market en numerosas aplicaciones, pero igual que pasaba con la action bar en versiones de
Android anteriores a la 3.0, se haca gracias a libreras externas que implementaban este
componente, o a diversas implementaciones ad-hoc, todas ellas distintas e incoherentes
entre s, tanto en diseo como en funcionalidad. Google ha querido acabar con esto
aportando su propia implementacin de este componente y definiendo su comportamiento
en las guas de diseo de la plataforma.
En este caso, en la documentacin oficial de Android (en ingls, por supuesto) est bastante
bien explicado cmo incluir este elemento en nuestras aplicaciones, pero an as voy a
detallarlo paso a paso en este artculo intercalando algunas notas que no aparecen en la
documentacin y que pueden evitaros algunas sorpresas.
El navigation drawer est disponible como parte de la librera de compatibilidad androidsupport. Para poder utilizarlo en nuestras aplicaciones tendremos que asegurarnos que
tenemos incluida en nuestro proyecto la librera android-support-v4.jar (debe ser la revisin
18 o superior, podemos comprobar cul tenemos instalada en el SDK Manager). Si tenemos
instalada una versin reciente del plugin de Android en Eclipse, al crear un nuevo proyecto
se aade directamente esta librera a la carpeta /lib.
Como ejemplo para este artculo, partir del cdigo ya construdo en el artculo
anterior sobre la Action Bar Compat, ya que es interesante ver cmo interactan ambos
elementos en la misma aplicacin y cmo podemos hacerlo compatible con todas las
versiones de Android.
Comprobado que tenemos incluida la librera android-suport, ya podemos comenzar a crear
nuestra aplicacin, y comenzaremos como siempre creando la interfaz de usuario. Para
aadir el navigation drawer a una actividad debemos hacer que el elemento raz del layout
159
las
las
en
un
XML). La forma de incluir estas opciones a la lista es mediante un adaptador normal, igual
que hemos comentado en ocasiones anteriores. Lo haremos por ejemplo dentro del
mtodo onCreate() de la actividad:
1
//...
2
import android.widget.ArrayAdapter;
3
import android.widget.ListView;
160
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarActivity;
public class MainActivity extends ActionBarActivity {
private String[] opcionesMenu;
private DrawerLayout drawerLayout;
private ListView drawerList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
opcionesMenu = new String[] {"Opcin 1", "Opcin 2", "Opcin 3"};
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
drawerList = (ListView) findViewById(R.id.left_drawer);
drawerList.setAdapter(new ArrayAdapter<String>(
getSupportActionBar().getThemedContext(),
android.R.layout.simple_list_item_1, opcionesMenu));
}
//...
}
Como podis ver en el cdigo anterior, el contexto pasado como parmetro al adaptador lo
hemos obtenido mediante una llamada al mtodo getThemedContext() de la action bar
(relevante tambin el haber usado getSupportActionBar() en vez de getActionBar() por
estar utilizando la versin de la action bar incluida en la librera de compatibilidad). Por
decirlo una forma sencilla, esto nos asegurar que los elementos de la lista se muestren
acordes al estilo de la action bar. Adems, vemos que como layout de los elementos de la
lista he utilizado el estandar android.R.layout.simple_list_item_1. Esto nos asegura
compatibilidad con la mayora de versiones de Android sin complicarnos mucho, aunque
tiene algunos problemas. Por ejemplo, la opcin seleccionada no se mantendr resaltada en
el men una vez se cierre y se vuelva a abrir. Para conseguir esto en Android 4 podemos
simplemente usar el layout android.R.layout.simple_list_item_activated_1, aunque
debemos tener en cuenta que la aplicacin generar un error si se ejecuta sobre versiones
anteriores. Para evitar este problema de compatibilidad tenemos varias opciones:
Utilizar un layout u otro dependiendo de la versin de Android sobre la que se est
ejecutando la aplicacin, asumiendo as que la opcin seleccionada se mantendr resaltada
en Android 4 pero no en versiones anteriores. Sera algo as:
1 drawerList.setAdapter(new
2 ArrayAdapter<String>(getSupportActionBar().getThemedContext(),
161
3
4
162
1
package net.sgoliver.android.navdrawer;
2
3
import android.os.Bundle;
4
import android.support.v4.app.Fragment;
5
import android.view.LayoutInflater;
6
import android.view.View;
7
import android.view.ViewGroup;
8
9
public class Fragment1 extends Fragment {
10
11
@Override
12
public View onCreateView(
13
LayoutInflater inflater, ViewGroup container,
14
Bundle savedInstanceState) {
15
16
return inflater.inflate(R.layout.fragment_1, container, false);
17
}
18 }
Los dos fragments restantes sern completamente anlogos al mostrado.
Ya tenemos listo el men y los fragments asociados a cada opcin. Lo siguiente ser
implementar la lgica necesaria para responder a los eventos del men de forma que
cambiemos de fragment al pulsar cada opcin. Esto lo haremos implementando el
evento onItemClick del control ListView del men, lgica que aadiremos al final del
mtodo onCreate() de nuestra actividad principal.
1
@Override
2
protected void onCreate(Bundle savedInstanceState) {
3
4
//...
5
6
drawerList.setOnItemClickListener(new OnItemClickListener() {
7
@Override
8
public void onItemClick(AdapterView parent, View view,
9
int position, long id) {
10
11
Fragment fragment = null;
12
13
switch (position) {
14
case 1:
15
fragment = new Fragment1();
16
break;
17
case 2:
18
fragment = new Fragment2();
19
break;
20
case 3:
163
21
fragment = new Fragment3();
22
break;
23
}
24
25
FragmentManager fragmentManager =
26
getSupportFragmentManager();
27
28
fragmentManager.beginTransaction()
29
.replace(R.id.content_frame, fragment)
30
.commit();
31
32
drawerList.setItemChecked(position, true);
33
34
tituloSeccion = opcionesMenu[position];
35
getSupportActionBar().setTitle(tituloSeccion);
36
37
drawerLayout.closeDrawer(drawerList);
38
}
39
});
40 }
Comentemos un poco el cdigo anterior. En primer lugar lo que hacemos es crear el nuevo
fragment a mostrar dependiendo de la opcin pulsada en el men de navegacin, que nos
llega como parmetro (position) del evento onItemClick. En el siguiente paso hacemos uso
del Fragment Manager (con getSupportFragmentManager() para hacer uso una vez ms de
la librera de compatibilidad) para sustituir el contenido del FrameLayout que definimos en
el layout de la actividad principal por el nuevo fragment creado. Posteriormente marcamos
como seleccionada la opcin pulsada de la lista mediante el mtodosetItemChecked(),
actualizamos el ttulo de la action bar por el de la opcin seleccionada, y por ltimo
cerramos el men llamando a closeDrawer().
Bien, pues ya tenemos la funcionalidad bsica implementada. Ahora nos quedara ajustar
algunos detalles para respetar las pautas definidas en la gua de diseo del componente
Navigation Drawer. Segn las recomendaciones de esta esta gua deberamos mostrar un
indicador en la action bar que evidencia al usuario la existencia del men lateral,
deberamos adems permitir al usuario abrirlo haciendo click en el icono de la aplicacin
(adems del gesto de deslizar desde el borde izquierdo hacia la derecha), y adicionalmente
cuando est abierto el men deberamos actualizar el ttulo de la action bar y ocultar
aquellas acciones relacionadas exclusivamente con el contenido principal actual
(semioculto por el men). La mayora de estas tareas las vamos a realizar ayudndonos de
una clase auxiliar llamadaActionBarDrawerToggle. Vayamos por partes.
164
165
A continuacin se implementan los eventos de apertura y cierre del men lateral. Lo nico
que haremos en estos eventos ser actualizar el ttulo de la action bar para mostrar el ttulo
de la aplicacin (cuando el men est abierto) o el ttulo de la opcin seleccionada
actualmente (cuando el men est cerrado). Adems, al final de cada uno de ellos hacemos
una llamada a invalidateOptionsMenu() para provocar que se ejecute el
evento onPrepareOptionsMenu() de la actividad, donde nos ocuparemos de ocultar las
acciones de la action bar que no apliquen cuando el men lateral est abierto. Importante
llamar a este mtodo haciendo uso de nuevo de su alternativa incluida en la librera de
compatibilidad, como mtodo de la clase ActivityCompat, ya que el
mtodo invalidateOptionsMenu() apareci con la API 11 (Android 3.0) y no funcionara en
versiones anteriores.
En nuestro caso de ejemplo ocultaremos la accin de buscar:
1
@Override
2
public boolean onPrepareOptionsMenu(Menu menu) {
3
4
boolean menuAbierto = drawerLayout.isDrawerOpen(drawerList);
5
6
if(menuAbierto)
7
menu.findItem(R.id.action_search).setVisible(false);
8
else
9
menu.findItem(R.id.action_search).setVisible(true);
10
11
return super.onPrepareOptionsMenu(menu);
12 }
Con esto ya cumplimos la mayora de las recomendaciones de la gua de diseo, pero an
nos falta permitir al usuario abrir el men pulsando sobre el icono de la aplicacin de la
action bar.
Para ello, al final del mtodo onCreate() habilitaremos la pulsacin del icono llamando a
los mtodossetDisplayHomeAsUpEnabled() y setHomeButtonEnabled(), y aadiremos al
eventoonOptionsItemSelected() (el encargado de procesar las pulsaciones sobre la action
bar,
una
llamada
inicial
al
mtodo onOptionsItemSelected() del
objeto ActionBarDrawerToggle creado anteriormente, de forma que si ste devuelve true
(significara que se ha gestionado una pulsacin sobre el icono de la aplicacin) salgamos
directamente de este mtodo.
1
public void onCreate(Bundle savedInstanceState) {
2
3
//...
4
166
5
6
7
8
9
10
11
12
13
14
15
16
17
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
//...
}
claseActionBarDrawerToggle:
Implementar el evento onPostCreate() de la actividad, donde llamando al
mtodo syncState() del objeto ActionBarDrawerToggle.
Implementar el evento onConfigurationChanged() de la actividad, donde llamaremos al
mtodo homlogo del objeto ActionBarDrawerToggle.
Veamos cmo quedaran el cdigo de estos dos ltimos pasos:
1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
drawerToggle.syncState();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
drawerToggle.onConfigurationChanged(newConfig);
}
167
Espero que estos dos ltimos artculos os hayan servido para aprender a construir
aplicaciones utilizando los componentes de diseo ms representativos de la plataforma
Android (la Action Bar y el Navigation Drawer) sin que por ello haya que restringirse a
versiones recientes del sistema operativo ni recurrir a libreras externas.
Mens en Android
Mens en Android (I): Conceptos bsicos
by Sgoliver on 21/03/2011 in Android, Programacin
En los dos siguientes artculos del Curso de Programacin Android nos vamos a centrar en
la creacin de mens de opciones en sus diferentes variantes.
NOTA IMPORTANTE: A partir de la versin 3 de Android los mens han caido en
desuso debido a la aparicin de la Action Bar. De hecho, si compilas tu aplicacin con un
target igual o superior a la API 11 (Android 3.0) vers como los mens que definas
aparecen, no en su lugar habitual en la parte inferior de la pantalla, sino en el men
desplegable de la action bar en la parte superior derecha. Por todo esto, lo que leers en este
artculo sobre mens y submens aplica tan slo a aplicaciones realizadas para Android 2.x
o inferior. Si quieres conocer ms detalles sobre la action bar tienes dos artculos dedicados
a ello en elndice del curso. De cualquier forma, este artculo sigue siendo til ya que la
forma de trabajar con la action bar se basa en la API de mens y en los recursos que
comentaremos en este texto.
En Android podemos encontrar 3 tipos diferentes de mens:
168
169
Como hemos comentado antes, este mismo men tambin lo podramos crear directamente
mediante cdigo, tambin desde el evento onCreateOptionsMenu(). Para ello, para aadir
cada opcin del men podemos utilizar el mtodo add() sobre el objeto de tipo Menu que
nos llega como parmetro del evento. Este mtodo recibe 4 parmetros: ID del grupo
170
171
12
13
14
15
16
return true;
default:
return super.onOptionsItemSelected(item);
}
}
Ojo, el cdigo anterior sera vlido para el men creado mediante XML. Si hubiramos
utilizado
la
implementacin por cdigo tendramos que
sustituir
las
constantes R.id.MnuOpc_ por nuestras constantesMNU_OPC_.
Con esto, hemos conseguido ya un men completamente funcional. Si ejecutamos el
proyecto en el emulador comprobaremos cmo al pulsar el botn de menu del telfono
aparece el men que hemos definido y que al pulsar cada opcin se muestra el mensaje de
ejemplo.
Pasemos ahora a comentar los submens. Un submen no es ms que un men secundario
que se muestra al pulsar una opcin determinada de un men principal. Los submens en
Android se muestran en forma de lista emergente, cuyo ttulo contiene el texto de la opcin
elegida en el men principal. Como ejemplo, vamos a aadir un submen a la Opcin 3 del
ejemplo anterior, al que aadiremos dos nuevas opciones secundarias. Para ello, bastar con
insertar en el XML de men un nuevo elemento <menu>dentro del item correspondiente a
la opcin 3. De esta forma, el XML quedara ahora como sigue:
1
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
2
<item android:id="@+id/MnuOpc1" android:title="Opcion1"
3
android:icon="@android:drawable/ic_menu_preferences"></item>
4
<item android:id="@+id/MnuOpc2" android:title="Opcion2"
5
android:icon="@android:drawable/ic_menu_compass"></item>
6
<item android:id="@+id/MnuOpc3" android:title="Opcion3"
7
android:icon="@android:drawable/ic_menu_agenda">
8
<menu>
9
<item android:id="@+id/SubMnuOpc1"
10
android:title="Opcion 3.1" />
11
<item android:id="@+id/SubMnuOpc2"
12
android:title="Opcion 3.2" />
13
</menu>
14
</item>
15 </menu>
Si volvemos a ejecutar ahora el proyecto y pulsamos la opcin 3 nos aparecer el
correspondiente submen con las dos nuevas opciones aadidas. Lo vemos en la siguiente
imagen:
172
Comprobamos como efectivamente aparecen las dos nuevas opciones en la lista emergente,
y que el ttulo de la lista se corresponde con el texto de la opcin elegida en el men
principal (Opcion3).
Para conseguir esto mismo mediante cdigo procederamos de forma similar a la anterior,
con la nica diferencia de que la opcin de men 3 la aadiremos utilizando el
mtodo addSubMenu() en vez de add(), y guardando una referencia al submenu. Sobre el
submen aadido insertaremos las dos nuevas opciones utilizando una vez ms el
mtodo add(). Vemos cmo quedara:
1
//Alternativa 2
2
menu.add(Menu.NONE, MNU_OPC1, Menu.NONE, "Opcion1")
3
.setIcon(android.R.drawable.ic_menu_preferences);
4
menu.add(Menu.NONE, MNU_OPC2, Menu.NONE, "Opcion2")
5
.setIcon(android.R.drawable.ic_menu_compass);
6
7
SubMenu smnu = menu.
8
addSubMenu(Menu.NONE, MNU_OPC1, Menu.NONE, "Opcion3")
9
.setIcon(android.R.drawable.ic_menu_agenda);
10 smnu.add(Menu.NONE, SMNU_OPC1, Menu.NONE, "Opcion 3.1");
11 smnu.add(Menu.NONE, SMNU_OPC2, Menu.NONE, "Opcion 3.2");
En cuanto a la implementacin de estas opciones de submen no habra diferencia con todo
lo
comentado
anteriormente
ya
que
tambin
se
tratan
desde
el
evento onOptionsItemSelected(), identificndolas por su ID.
Por tanto, con esto habramos terminado de comentar las opciones bsicas a la hora de crear
mens y submenus en nuestras aplicaciones Android. En el siguiente artculo veremos
algunas opciones algo ms avanzadas que, aunque menos frecuentes, puede que nos hagan
falta para desarrollar determinadas aplicaciones.
173
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
Si necesitis iconos para mostrar en los mens aqu tenis varios enlaces con algunos
gratuitos que podis utilizar en vuestras aplicaciones Android:
http://www.androidicons.com/freebies.php
http://www.glyfx.com/products/free_android2.html
174
5
//Obtenemos las referencias a los controles
6
lblMensaje = (TextView)findViewById(R.id.LblMensaje);
7
8
//Asociamos los mens contextuales a los controles
9
registerForContextMenu(lblMensaje);
10 }
A continuacin, igual que hacamos con onCreateOptionsMenu() para los mens bsicos,
vamos a sobreescribir en nuestra actividad el evento encargado de construir los mens
contextuales asociados a los diferentes controles de la aplicacin. En este caso el evento se
llama onCreateContextMenu(), y a diferencia de onCreateOptionsMenu() ste se llama
cada vez que se necesita mostrar un men contextual, y no una sola vez al inicio de la
aplicacin. En este evento actuaremos igual que para los mnus bsicos, inflando el men
XML que hayamos creado con las distintas opciones, o creando a mano el men mediante
el mtodo add() [para ms informacin leer el artculo anterior]. En nuestro ejemplo hemos
definido un men en XML llamado menu_ctx_etiqueta.xml:
1
<?xml version="1.0" encoding="utf-8"?>
2
<menu
3
xmlns:android="http://schemas.android.com/apk/res/android">
4
5
<item android:id="@+id/CtxLblOpc1"
6
android:title="OpcEtiqueta1"></item>
7
<item android:id="@+id/CtxLblOpc2"
8
android:title="OpcEtiqueta2"></item>
9
10 </menu>
Por su parte el evento onCreateContextMenu() quedara de la siguiente forma:
1 @Override
2 public void onCreateContextMenu(ContextMenu menu, View v,
3
ContextMenuInfo menuInfo)
4 {
5
super.onCreateContextMenu(menu, v, menuInfo);
6
7
MenuInflater inflater = getMenuInflater();
8
inflater.inflate(R.menu.menu_ctx_etiqueta, menu);
9 }
Por ltimo, para implementar las acciones a realizar tras pulsar una opcin determinada del
men contextual vamos a implementar el evento onContextItemSelected() de forma
anloga a cmo hacamos con onOptionsItemSelected() para los mens bsicos:
1
@Override
2
public boolean onContextItemSelected(MenuItem item) {
3
4
switch (item.getItemId()) {
5
case R.id.CtxLblOpc1:
175
6
7
8
9
10
11
12
13
14
Con esto, ya tendramos listo nuestro men contextual para la etiqueta de texto de la
actividad principal, y como veis todo es prcticamente anlogo a cmo construimos los
mens y submens bsicos en el artculo anterior. En este punto ya podramos ejecutar el
proyecto en el emulador y comprobar su funcionamiento. Para ello, una vez iniciada la
aplicacin tan slo tendremos que realizar una pulsacin larga sobre la etiqueta de texto. En
ese momento debera aparecer el men contextual, donde podremos seleccionar cualquier
de las dos opciones definidas.
Ahora vamos con algunas particularidades. Los mens contextuales se utilizan a menudo
con controles de tipo lista, lo que aade algunos detalles que conviene mencionar. Para ello
vamos a aadir a nuestro ejemplo una lista con varios datos de muestra y vamos a asociarle
un nuevo men contextual. Modificaremos el layout XML de la ventana principal para
aadir el control ListView y modificaremos el mtodo onCreate() para obtener la referencia
al control, insertar vaios datos de ejemplo, y asociarle un men contextual:
1
public void onCreate(Bundle savedInstanceState) {
2
super.onCreate(savedInstanceState);
3
setContentView(R.layout.main);
4
5
//Obtenemos las referencias a los controles
6
lblMensaje = (TextView)findViewById(R.id.LblMensaje);
7
lstLista = (ListView)findViewById(R.id.LstLista);
8
9
//Rellenamos la lista con datos de ejemplo
10
String[] datos =
11
new String[]{"Elem1","Elem2","Elem3","Elem4","Elem5"};
12
13
ArrayAdapter<String> adaptador =
14
new ArrayAdapter<String>(this,
15
android.R.layout.simple_list_item_1, datos);
16
17
lstLista.setAdapter(adaptador);
18
19
//Asociamos los mens contextuales a los controles
20
registerForContextMenu(lblMensaje);
176
21
registerForContextMenu(lstLista);
22 }
Como en el caso anterior, vamos a definir en XML otro men para asociarlo a los
elementos de la lista, lo llamaremos menu_ctx_lista:
1
<?xml version="1.0" encoding="utf-8"?>
2
<menu
3
xmlns:android="http://schemas.android.com/apk/res/android">
4
5
<item android:id="@+id/CtxLstOpc1"
6
android:title="OpcLista1"></item>
7
<item android:id="@+id/CtxLstOpc2"
8
android:title="OpcLista2"></item>
9
10 </menu>
Como siguiente paso, y dado que vamos a tener varios mens contextuales en la misma
actividad, necesitaremos modificar el evento onCreateContextMenu() para que se construya
un men distinto dependiendo del control asociado. Esto lo haremos obteniendo el ID del
control al que se va a asociar el men contextual, que se recibe en forma de parmetro
(View v) en el evento onCreateContextMenu(). Utilizaremos para ello una llamada al
mtodo getId() de dicho parmetro:
1
@Override
2
public void onCreateContextMenu(ContextMenu menu, View v,
3
ContextMenuInfo menuInfo)
4
{
5
super.onCreateContextMenu(menu, v, menuInfo);
6
7
MenuInflater inflater = getMenuInflater();
8
9
if(v.getId() == R.id.LblMensaje)
10
inflater.inflate(R.menu.menu_ctx_etiqueta, menu);
11
else if(v.getId() == R.id.LstLista)
12
{
13
AdapterView.AdapterContextMenuInfo info =
14
(AdapterView.AdapterContextMenuInfo)menuInfo;
15
16
menu.setHeaderTitle(
17
lstLista.getAdapter().getItem(info.position).toString());
18
19
inflater.inflate(R.menu.menu_ctx_lista, menu);
20
}
21 }
Vemos cmo en el caso del men para el control lista hemos ido adems un poco ms all,
y hemos personalizado el ttulo del men contextual [mediante setHeaderTitle()] para que
177
muestre el texto del elemento seleccionado en la lista. Para hacer esto nos hace falta saber
la posicin en la lista del elemento seleccionado, algo que podemos conseguir haciendo uso
del ltimo parmetro recibido en el eventoonCreateContextMenu(), llamado menuInfo.
Este parmetro contiene informacin adicional del control que se ha pulsado para mostrar
el men contextual, y en el caso particular del control ListView contiene la posicin del
elemento concreto de la lista que se ha pulsado. Para obtenerlo, convertimos el
parmetromenuInfo a un objeto de tipo AdapterContextMenuInfo y accedemos a su
atributo position tal como vemos en el cdigo anterior.
La respuesta a este nuevo men se realizar desde el mismo evento que el anterior, todo
dentro deonContextItemSelected(). Por tanto, incluyendo las opciones del nuevo men
contextual para la lista el cdigo nos quedara de la siguiente forma:
1
@Override
2
public boolean onContextItemSelected(MenuItem item) {
3
4
AdapterContextMenuInfo info =
5
(AdapterContextMenuInfo) item.getMenuInfo();
6
7
switch (item.getItemId()) {
8
case R.id.CtxLblOpc1:
9
lblMensaje.setText("Etiqueta: Opcion 1 pulsada!");
10
return true;
11
case R.id.CtxLblOpc2:
12
lblMensaje.setText("Etiqueta: Opcion 2 pulsada!");
13
return true;
14
case R.id.CtxLstOpc1:
15
lblMensaje.setText("Lista[" + info.position + "]: Opcion 1 pulsada!");
16
return true;
17
case R.id.CtxLstOpc2:
18
lblMensaje.setText("Lista[" + info.position + "]: Opcion 2 pulsada!");
19
return true;
20
default:
21
return super.onContextItemSelected(item);
22
}
23 }
Como
vemos,
aqu
tambin
utilizamos
la
informacin
del
objeto AdapterContextMenuInfo para saber qu elemento de la lista se ha pulsado, con la
nica diferencia de que en esta ocasin lo obtenemos mediante una llamada al
mtodo getMenuInfo() de la opcin de men (MenuItem) recibida como parmetro.
Si volvemos a ejecutar el proyecto en este punto podremos comprobar el aspecto de nuestro
men contextual al pulsar cualquier elemento de la lista:
178
A modo de resumen, en este artculo hemos visto cmo crear mens contextuales asociados
a determinados elementos y controles de nuestra interfaz de la aplicacin. Hemos visto
cmo crear mens bsicos y algunas particularidades que existen a la hora de asociar mens
contextuales a elementos de un control de tipo lista. Para no alargar este artculo
dedicaremos un tercero a comentar algunas opciones menos frecuentes, pero igualmente
tiles, de los mens en Android.
Mens en Android (III): Opciones avanzadas
by Sgoliver on 06/10/2011 in Android, Programacin
179
En los artculos anteriores del curso ya hemos visto cmo crear mens bsicos para nuestras
aplicaciones, tanto mens principales como de tipo contextual. Sin embargo, se nos
quedaron en el tintero un par de temas que tambin nos pueden ser necesarios o interesantes
a la hora de desarrollar una aplicacin. Por un lado veremos los grupos de opciones, y por
otro la actualizacin dinmica de un men segn determinadas condiciones.
Los grupos de opciones son un mecanismo que nos permite agrupar varios elementos de un
men de forma que podamos aplicarles ciertas acciones o asignarles determinadas
caractersticas o funcionalidades de forma conjunta. De esta forma, podremos por ejemplo
habilitar o deshabilitar al mismo tiempo un grupo de opciones de men, o hacer que slo se
pueda seleccionar una de ellas. Lo veremos ms adelante.
Veamos primero cmo definir un grupo de opciones de men. Como ya comentamos,
Android nos permite definir un men de dos formas distintas: mediante un fichero XML, o
directamente a travs de cdigo. Si elegimos la primera opcin, para definir un grupo de
opciones nos basta con colocar dicho grupo dentro de un elemento <group>, al que
asignaremos un ID. Veamos un ejemplo. Vamos a definir un men con 3 opciones
principales, donde la ltima opcin abre un submen con 2 opciones que formen parte de
un grupo. A todas las opciones le asignaremos un ID y un texto, y a las opciones
principales asignaremos adems una imagen.
1
<menu
2
xmlns:android="http://schemas.android.com/apk/res/android">
3
4
<item android:id="@+id/MnuOpc1" android:title="Opcion1"
5
android:icon="@android:drawable/ic_menu_preferences"></item>
6
<item android:id="@+id/MnuOpc2" android:title="Opcion2"
7
android:icon="@android:drawable/ic_menu_compass"></item>
8
<item android:id="@+id/MnuOpc3" android:title="Opcion3"
9
android:icon="@android:drawable/ic_menu_agenda">
10
<menu>
11
<group android:id="@+id/grupo1">
12
13
<item android:id="@+id/SubMnuOpc1"
14
android:title="Opcion 3.1" />
15
<item android:id="@+id/SubMnuOpc2"
16
android:title="Opcion 3.2" />
17
18
</group>
19
</menu>
20 </item>
21
22 </menu>
180
Como vemos, las dos opciones del submen se han incluido dentro de un
elemento <group>. Esto nos permitir ejecutar algunas acciones sobre todas las opciones
del grupo de forma conjunta, por ejemplo deshabilitarlas u ocultarlas:
1 //Deshabilitar todo el grupo
2 mnu.setGroupEnabled(R.id.grupo1, false);
3
4 //Ocultar todo el grupo
5 mnu.setGroupVisible(R.id.grupo1, false);
Adems de estas acciones, tambin podemos modificar el comportamiento de las opciones
del grupo de forma que tan slo se pueda seleccionar una de ellas, o para que se puedan
seleccionar varias. Con esto convertiramos el grupo de opciones de men en el equivalente
a un conjunto de controles RadioButton oCheckBox respectivamente. Esto lo conseguimos
utilizando el atributo android:checkableBehavior del elemento <group>, al que podemos
asignar el valor single (seleccin exclusiva, tipo RadioButton) o all (seleccin mltiple,
tipo CheckBox). En nuestro caso de ejemplo vamos a hacer seleccionable slo una de las
opciones del grupo:
1 <group android:id="@+id/grupo1" android:checkableBehavior="single">
2
3
<item android:id="@+id/SubMnuOpc1"
4
android:title="Opcion 3.1" />
5
<item android:id="@+id/SubMnuOpc2"
6
android:title="Opcion 3.2" />
7
8 </group>
Si optamos por construir el men directamente mediante cdigo debemos utilizar el
mtodosetGroupCheckable() al que pasaremos como parmetros el ID del grupo y el tipo
de seleccin que deseamos (exclusiva o no). As, veamos el mtodo de construccin del
men anterior mediante cdigo:
1
private static final int MNU_OPC1 = 1;
2
private static final int MNU_OPC2 = 2;
3
private static final int MNU_OPC3 = 3;
4
private static final int SMNU_OPC1 = 31;
5
private static final int SMNU_OPC2 = 32;
6
7
private static final int GRUPO_MENU_1 = 101;
8
9
private int opcionSeleccionada = 0;
10
11 //...
12
13 private void construirMenu(Menu menu)
14 {
181
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
MNU_OPC3,
Como vemos, al final del mtodo nos ocupamos de marcar manualmente la opcin
seleccionada actualmente, que debemos conservar en algn atributo interno (en mi caso lo
he llamadoopcionSeleccionada) de nuestra actividad. Esta marcacin manual la hacemos
mediante el mtodogetItem() para obtener una opcin determinada del submen y sobre
sta el mtodo setChecked()para establecer su estado. Por qu debemos hacer esto? No
guarda Android el estado de las opciones de menu seleccionables? La respuesta es s, s lo
hace, pero siempre que no reconstruyamos el men entre una visualizacin y otra. Pero no
dijimos que la creacin del men slo se realiza una vez en la primera llamada
a onCreateOptionsMenu()? Tambin es cierto, pero despus veremos cmo tambin es
posible preparar nuestra aplicacin para poder modificar de forma dinmica un men segn
determinadas condiciones, lo que s podra implicar reconstruirlo previamente a cada
visualizacin. En definitiva, si guardamos y restauramos nosotros mismos el estado de las
182
El segundo tema que quera desarrollar en este artculo trata sobre la modificacin dinmica
de un men durante la ejecucucin de la aplicacin de forma que ste sea distinto segun
determinadas condiciones. Supongamos por ejemplo que normalmente vamos a querer
mostrar nuestro men con 3 opciones, pero si tenemos marcada en pantalla una
determinada opcin queremos mostrar en el men una opcin adicional. Cmo hacemos
183
esto si dijimos que el evento onCreateOptionsMenu() se ejecuta una sola vez? Pues esto es
posible
ya
que
adems
del
evento
indicado
existe
otro
llamado onPrepareOptionsMenu() que se ejecuta cada vez que se va a mostrar el men de
la aplicacin, con lo que resulta el lugar ideal para adaptar nuestro men a las condiciones
actuales de la aplicacin.
Para mostrar el funcionamiento de esto vamos a colocar en nuestra aplicacin de ejemplo
un nuevo checkbox (lo llamar en mi caso chkMenuExtendido). Nuestra intencin es que si
este checkbox est marcado el men muestre una cuarta opcin adicional, y en caso
contrario slo muestre las tres opciones ya vistas en los ejemplos anteriores.
En primer lugar prepararemos el mtodo construirMenu() para que reciba un parmetro
adicional que indique si queremos construir un men extendido o no, y slo aadiremos la
cuarta opcin si este parmetro llega activado.
private void construirMenu(Menu menu, boolean extendido)
1
{
2
menu.add(Menu.NONE, MNU_OPC1, Menu.NONE, "Opcion1")
3
.setIcon(android.R.drawable.ic_menu_preferences);
4
menu.add(Menu.NONE, MNU_OPC2, Menu.NONE, "Opcion2")
5
.setIcon(android.R.drawable.ic_menu_compass);
6
7
SubMenu
smnu
=
menu.addSubMenu(Menu.NONE,
MNU_OPC3,
8
Menu.NONE, "Opcion3")
9
.setIcon(android.R.drawable.ic_menu_agenda);
10
11
smnu.add(GRUPO_MENU_1, SMNU_OPC1, Menu.NONE, "Opcion 3.1");
12
smnu.add(GRUPO_MENU_1, SMNU_OPC2, Menu.NONE, "Opcion 3.2");
13
14
//Establecemos la seleccin exclusiva para el grupo de opciones
15
smnu.setGroupCheckable(GRUPO_MENU_1, true, true);
16
17
if(extendido)
18
menu.add(Menu.NONE, MNU_OPC4, Menu.NONE, "Opcion4")
19
.setIcon(android.R.drawable.ic_menu_camera);
20
21
//Marcamos la opcin seleccionada actualmente
22
if(opcionSeleccionada == 1)
23
smnu.getItem(0).setChecked(true);
24
else if(opcionSeleccionada == 2)
25
smnu.getItem(1).setChecked(true);
26
}
Adems de esto, implementaremos el evento onPrepareOptionsMenu() para que llame a
este mtodo de una forma u otra dependiendo del estado del nuevo checkbox.
1
@Override
184
2
3
4
5
6
7
8
9
10
11
12
Como vemos, en primer lugar debemos resetear el men mediante el mtodo clear() y
posteriormente llamar de nuevo a nuestro mtodo de construccin del men indicando si
queremos un men extendido o no segn el valor de la check.
Si ejecutamos nuevamente la aplicacin de ejemplo, marcamos el checkbox y mostramos la
tecla de men podremos comprobar cmo se muestra correctamente la cuarta opcin
aadida.
Y con esto cerramos ya todos los temas referentes a mens que tena intencin de incluir en
este Curso de Programacin en Android. Espero que sea suficiente para cubrir las
necesidades de muchas de vuestras aplicaciones.
Widgets en Android
Interfaz de usuario en Android: Widgets (I)
by Sgoliver on 23/02/2011 in Android, Programacin
185
En los dos prximos artculos del Curso de Programacin Android vamos a describir cmo
crear un widget de escritorio (home screen widget).
En esta primera parte construiremos un widget esttico (no ser interactivo, ni contendr
datos actualizables, ni responder a eventos) muy bsico para entender claramente la
estructura interna de un componente de este tipo, y en el siguiente artculo completaremos
el ejercicio aadiendo una ventana de configuracin inicial para el widget, aadiremos
algn dato que podamos actualizar periodicamente, y haremos que responda a pulsaciones
del usuario.
Como hemos dicho, en esta primera parte vamos a crear un widget muy bsico, consistente
en un simple marco rectangular negro con un mensaje de texto predeterminado (Mi Primer
Widget). La sencillez del ejemplo nos permitir centrarnos en los pasos principales de la
construccin de un widget Android y olvidarnos de otros detalles que nada tienen que ver
con el tema que nos ocupa (grficos, datos, ). Para que os hagis una idea, ste ser el
aspecto final de nuestro widget de ejemplo:
Los pasos principales para la creacin de un widget Android son los siguientes:
1. Definicin de su interfaz grfica (layout).
2. Configuracin XML del widget (AppWidgetProviderInfo).
3. Implementacin de la funcionalidad del widget (AppWidgetProvider) , especialmente su
evento de actualizacin.
4. Declaracin del widget en el Android Manifest de la aplicacin.
En el primer paso no nos vamos a detener mucho ya que es anlogo a cualquier definicin
de layout de las que hemos visto hasta ahora en el curso. En esta ocasin, la interfaz del
widget estar compuesta nicamente por un par de frames (FrameLayout), uno negro
exterior y uno blanco interior algo ms pequeo para simular el marco, y una etiqueta de
186
texto (TextView) que albergar el mensaje a mostrar. Veamos cmo queda el layout xml,
que para este ejemplo llamaremos miwidget.xml:
1
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
android:layout_width="match_parent"
3
android:layout_height="match_parent"
4
android:background="#000000"
5
android:padding="10dp"
6
android:layout_margin="5dp" >
7
8
<FrameLayout android:id="@+id/frmWidget"
9
android:layout_width="match_parent"
10
android:layout_height="match_parent"
11
android:background="#FFFFFF"
12
android:padding="5dp" >
13
14
<TextView android:id="@+id/txtMensaje"
15
android:layout_width="match_parent"
16
android:layout_height="match_parent"
17
android:textColor="#000000"
18
android:text="@string/mi_primer_widget" />
19
20
</FrameLayout>
21
22 </FrameLayout>
Cabe destacar aqu que, debido a que el layout de los widgets de Android est basado en un
tipo especial de componentes llamados RemoteViews, no es posible utilizar en su interfaz
todos los contenedores y controles que hemos visto en artculos anteriores sino slo unos
pocos bsicos que se indican a continuacin:
Contenedores: FrameLayout, LinearLayout, RelativeLayout y GridLayout (ste ltimo a
partir de Android 4).
Controles: Button, ImageButton, ImageView, TextView, ProgressBar, Chronometer, Analo
gClocky ViewFlipper. A partir de Android 3 tambin podemos utilizar ListView,
GridView, StackView y AdapterViewFlipper, aunque su uso tiene algunas particularidades.
En este artculo no trataremos este ltimo caso, pero si necesitas informacin puedes
empezar por la documentacin oficial sobre el tema.
Aunque la lista de controles soportados no deja de ser curiosa (al menos en mi humilde
opinin), debera ser suficiente para crear todo tipo de widgets.
Como segundo paso del proceso de construccin vamos a crear un nuevo fichero XML
donde definiremos un conjunto de propiedades del widget, como por ejemplo su tamao en
pantalla o su frecuencia de actualizacin. Este XML se deber crear en la
carpeta \res\xml de nuestro proyecto. En nuestro caso de ejemplo lo llamaremos
miwidget_wprovider.xml y tendr la siguiente estructura:
187
188
189
vamos a ver cmo podemos aadir los siguientes elementos y funcionalidades al widget
bsico que ya construmos:
Pantalla de configuracin inicial.
Datos actualizables de forma periodica.
Eventos de usuario.
190
Como sabis, intento simplificar al mximo todos los ejemplos que utilizo en este curso
para que podamos centrar nuestra atencin en los aspectos realmente importantes. En esta
ocasin utilizar el mismo criterio y las nicas caractersticas (aunque suficientes para
demostrar los tres conceptos anteriores) que aadiremos a nuestro widget sern las
siguientes:
1. Aadiremos una pantalla de configuracin inicial del widget, que aparecer cada vez que se
aada una nueva instancia del widget a nuestro escritorio. En esta pantalla podr
configurarse nicamente el mensaje de texto a mostrar en el widget.
2. Aadiremos un nuevo elemento de texto al widget que muestre la hora actual. Esto nos
servir para comprobar que el widget se actualiza periodicamente.
3. Aadiremos un botn al widget, que al ser pulsado forzar la actualizacin inmediata del
mismo.
Empecemos por el primer punto, la pantalla de configuracin inicial del widget. Y
procederemos igual que para el diseo de cualquier otra actividad android, definiendo su
layout xml. En nuestro caso ser muy sencilla, un cuadro de texto para introducir el
mensaje a personalizar y dos botones, uno para aceptar la configuracin y otro para
cancelar (en cuyo caso el widget no se aade al escritorio). En esta ocasin llamaremos a
este layout widget_config.xml. Veamos como queda:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView android:id="@+id/LblMensaje"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/mensaje_personalizado" />
<EditText android:id="@+id/TxtMensaje"
android:layout_height="wrap_content"
android:layout_width="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<Button android:id="@+id/BtnAceptar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/aceptar" />
191
<Button android:id="@+id/BtnCancelar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancelar" />
</LinearLayout>
</LinearLayout>
Una vez diseada la interfaz de nuestra actividad de configuracin tendremos que
implementar su funcionalidad en java. Llamaremos a la clase WidgetConfig, su estructura
ser anloga a la de cualquier actividad de Android, y las acciones a realizar sern las
comentadas a continuacin. En primer lugar nos har falta el identificador de la instancia
concreta del widget que se configurar con esta actividad. Este ID nos llega como
parmetro del intent que ha lanzado la actividad. Como ya vimos en un artculo anterior del
curso, este intent se puede recuperar mediante el mtdo getIntent() y sus parmetros
mediante el mtodo getExtras(). Conseguida la lista de parmetros del intent, obtendremos
el
valor
del
ID
del
widget
accediendo
a
la
clave AppWidgetManager.EXTRA_APPWIDGET_ID. Veamos el cdigo hasta este
momento:
public class WidgetConfig extends Activity {
private int widgetId = 0;
private Button btnAceptar;
private Button btnCancelar;
private EditText txtMensaje;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.widget_config);
//Obtenemos el Intent que ha lanzado esta ventana
//y recuperamos sus parmetros
Intent intentOrigen = getIntent();
Bundle params = intentOrigen.getExtras();
//Obtenemos el ID del widget que se est configurando
widgetId = params.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
192
193
patrn msg_IdWidget, esto nos permitir distinguir el mensaje configurado para cada
instancia del widget que aadamos a nuestro escritorio de Android.
El segundo paso indicado es necesario debido a que si definimos una actividad de
configuracin para un widget, ser sta la que tenga la responsabilidad de realizar la
primera actualizacin del mismo en caso de ser necesario. Es decir, tras salir de la actividad
de configuracin no se lanzar automticamente el eventoonUpdate() del widget (s se
lanzar posteriormente y de forma peridica segn la configuracin del
parmetro updatePeriodMillis del provider que veremos ms adelante), sino que tendr que
ser la propia actividad quien fuerce la primera actualizacin. Para ello, simplemente
obtendremos una referencia al widget manager de nuestro contexto mediente el
mtodo AppWidgetManager.getInstance() y con esta referencia llamaremos al mtodo
esttico de actualizacin del widgetMiWidget.actualizarWidget(), que actualizar los datos
de todos los controles del widget (lo veremos un poco ms adelante).
Por ltimo, al resultado a devolver (RESULT_OK) deberemos aadir informacin sobre el
ID de nuestro widget. Esto lo conseguimos creando un nuevo Intent que contenga como
parmetro el ID del widget que recuperamos antes y establecindolo como resultado de la
actividad mediante el mtodosetResult(resultado, intent). Por ltimo llamaremos al
mtodo finish() para finalizar la actividad.
Con estas indicaciones, veamos cmo quedara el cdigo del botn Aceptar:
//Implementacin del botn "Aceptar"
btnAceptar.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
//Guardamos el mensaje personalizado en las preferencias
SharedPreferences prefs =
getSharedPreferences("WidgetPrefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("msg_" + widgetId, txtMensaje.getText().toString());
editor.commit();
//Actualizamos el widget tras la configuracin
AppWidgetManager appWidgetManager =
AppWidgetManager.getInstance(WidgetConfig.this);
MiWidget.actualizarWidget(WidgetConfig.this, appWidgetManager, widgetId);
//Devolvemos como resultado: ACEPTAR (RESULT_OK)
Intent resultado = new Intent();
resultado.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
setResult(RESULT_OK, resultado);
finish();
}
194
});
Ya hemos terminado de implementar nuestra actividad de configuracin. Pero para su
correcto funcionamiento an nos quedan dos detalles ms por modificar. En primer lugar
tendremos que declarar esta actividad en nuestro fichero AndroidManifest.xml, indicando
que debe responder a los mensajes de tipo APPWIDGET_CONFIGURE:
<activity android:name=".WidgetConfig">
<intent-filter>
<action android:name="android.apwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
Por ltimo, debemos indicar en el XML de configuracin de nuestro widget
(xml\miwidget_wprovider.xml) que al aadir una instancia de este widget debe mostrarse la
actividad de configuracin que hemos creado. Esto se consigue estableciendo el
atributo android:configure del provider. Aprovecharemos adems este paso para establecer
el tiempo de actualizacin automtica del widget al mnimo permitido por este parmetro
(30 minutos) y el tamao del widget a 32 celdas. Veamos cmo quedara finalmente:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/miwidget"
android:minWidth="180dip"
android:minHeight="110dip"
android:label="@string/mi_primer_widget"
android:updatePeriodMillis="3600000"
android:configure="net.sgoliver.android.widgets.WidgetConfig"
/>
Con esto, ya tenemos todo listo para que al aadir nuestro widget al escritorio se muestre
automticamente la pantalla de configuracin que hemos construido. Podemos ejecutar el
proyecto en este punto y comprobar que todo funciona correctamente.
Como siguiente paso vamos a modificar el layout del widget que ya construimos en el
artculo anterior para aadir una nueva etiqueta de texto donde mostraremos la hora actual,
y un botn que nos servir para forzar la actualizacin de los datos del widget:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:padding="10dp"
android:layout_margin="5dp" >
<LinearLayout android:id="@+id/FrmWidget"
android:layout_width="match_parent"
android:layout_height="match_parent"
195
android:background="#FFFFFF"
android:padding="5dp"
android:orientation="vertical">
<TextView android:id="@+id/LblMensaje"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:text="" />
<TextView android:id="@+id/LblHora"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:text="" />
<Button android:id="@+id/BtnActualizar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:text="@string/actualizar" />
</LinearLayout>
</FrameLayout>
Hecho esto, tendremos que modificar la implementacin de nuestro provider
(MiWidget.java) para que en cada actualizacin del widget se actualicen sus controles con
los datos correctos (recordemos que en el artculo anterior dejamos este evento de
actualizacin vaco ya que no mostrbamos datos actualizables en el widget). Esto lo
haremos dentro del evento onUpdate() de nuestro provider.
Como ya dijimos, los componentes de un widget se basan en un tipo especial de vistas que
llamamos Remote Views. Pues bien, para acceder a la lista de estos componentes que
constituyen la interfaz del widget construiremos un nuevo objeto RemoteViews a partir del
ID del layout del widget. Obtenida la lista de componentes, tendremos disponibles una serie
de mtodos set (uno para cada tipo de datos bsicos) para establecer las propiedades de
cada control del widget. Estos mtodos reciben como parmetros el ID del control, el
nombre del mtodo que queremos ejecutar sobre el control, y el valor a establecer. Adems
de estos mtodos, contamos adicionalmente con una serie de mtodos ms especficos para
establecer directamente el texto y otras propiedades sencillas de los
controles TextView, ImageView, ProgressBar yChronometer,
como
por
ejemplo setTextViewText(idControl, valor) para establecer el textode un control TextView.
196
197
198
199
el ID del widget que lo ha lanzado, obtendremos una referencia al widget manager, y por
ltimo llamaremos a nuestro mtodo esttico de actualizacin pasndole estos datos.
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("net.sgoliver.android.widgets.ACTUALIZAR_WIDGET"))
{
//Obtenemos el ID del widget a actualizar
int widgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
//Obtenemos el widget manager de nuestro contexto
AppWidgetManager widgetManager =
AppWidgetManager.getInstance(context);
//Actualizamos el widget
if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
actualizarWidget(context, widgetManager, widgetId);
}
}
Con esto, por fin, hemos ya finalizado la construccin de nuestro widget android y
podemos ejecutar el proyecto de Eclipse para comprobar que todo funciona correctamente,
tanto para una sola instancia como para varias instancias simultaneas.
Cuando aadamos el widget al escritorio nos aparecer la pantalla de configuracin que
hemos definido:
200
Una vez introducido el mensaje que queremos mostrar, pulsaremos el botn Aceptar y el
widget aparecer automticamente en el escritorio con dicho mensaje, la fecha-hora actual
y el botn Actualizar.
201
no slo para la construccin de widgets, sino para cualquier tipo de aplicacin Android.
Este tema es la administracin de preferencias.
Las preferencias no son ms que datos que una aplicacin debe guardar para personalizar la
experiencia del usuario, por ejemplo informacin personal, opciones de presentacin, etc.
En artculos anteriores vimos ya uno de los mtodos disponibles en la plataforma Android
para almacenar datos, como son las bases de datos SQLite. Las preferencias de una
aplicacin se podran almacenar por su puesto utilizando este mtodo, y no tendra nada de
malo, pero Android proporciona otro mtodo alternativo diseado especficamente para
administrar este tipo de datos: las preferencias compartidas o shared preferences. Cada
preferencia se almacenar en forma de clave-valor, es decir, cada una de ellas estar
compuesta por un identificador nico (p.e. email) y un valor asociado a dicho
identificador (p.e. prueba@email.com). Adems, y a diferencia de SQLite, los datos no
se guardan en un fichero binario de base de datos, sino en ficheros XML como veremos al
final de este artculo.
La API para el manejo de estas preferencias es muy sencilla. Toda la gestin se centraliza
en la claseSharedPrefences, que representar a una coleccin de preferencias. Una
aplicacin Android puede gestionar varias colecciones de preferencias, que se diferenciarn
mediante un identificador nico. Para obtener una referencia a una coleccin determinada
utilizaremos el mtodo getSharedPrefences() al que pasaremos el identificador de la
coleccin y un modo de acceso. El modo de acceso indicar qu aplicaciones tendrn
acceso a la coleccin de preferencias y qu operaciones tendrn permitido realizar sobre
202
Una vez hemos obtenido una referencia a nuestra coleccin de preferencias, ya podemos
obtener, insertar o modificar preferencias utilizando los mtodos get o put correspondientes
al tipo de dato de cada preferencia. As, por ejemplo, para obtener el valor de una
preferencia llamada email de tipo Stringescribiramos lo siguiente:
SharedPreferences prefs =
getSharedPreferences("MisPreferencias",Context.MODE_PRIVATE);
String correo = prefs.getString("email", "por_defecto@email.com");
Como vemos, al mtodo getString() le pasamos el nombre de la preferencia que queremos
recuperar y un segundo parmetro con un valor por defecto. Este valor por defecto ser el
devuelto por el mtodogetString() si la preferencia solicitada no existe en la coleccin.
Adems del mtodo getString(), existen por supuesto mtodos anlogos para el resto de
tipos de datos bsicos, por ejemplo getInt(),getLong(), getFloat(), getBoolean(),
Para actualizar o insertar nuevas preferencias el proceso ser igual de sencillo, con la nica
diferencia de que la actualizacin o insercin no la haremos directamente sobre el
objeto SharedPreferences, sino sobre su objeto de edicin SharedPreferences.Editor. A este
ltimo objeto accedemos mediante el mtodo edit() de la clase SharedPreferences. Una vez
obtenida la referencia al editor, utilizaremos los mtodos put correspondientes al tipo de
datos de cada preferencia para actualizar/insertar su valor, por ejemplo putString(clave,
valor), para actualizar una preferencia de tipo String. De forma anloga a los
mtodos get que ya hemos visto, tendremos disponibles mtodos put para todos los tipos de
datos
bsicos: putInt(), putFloat(), putBoolean(),
etc.
Finalmente,
una
vez
actualizados/insertados todos los datos necesarios llamaremos al mtodo commit() para
confirmar los cambios. Veamos un ejemplo sencillo:
SharedPreferences prefs =
getSharedPreferences("MisPreferencias",Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("email", "modificado@email.com");
editor.putString("nombre", "Prueba");
editor.commit();
Pero donde se almacenan estas preferencias compartidas? Como dijimos al comienzo del
artculo, las preferencias no se almacenan en ficheros binarios como las bases de datos
SQLite, sino en ficheros XML. Estos ficheros XML se almacenan en una ruta que sigue el
siguiente patrn:
/data/data/paquete.java/shared_prefs/nombre_coleccion.xml
As, por ejemplo, en nuestro caso encontraramos nuestro fichero de preferencias en la ruta:
203
/data/data/net.sgoliver.android.preferences1/shared_prefs/MisPreferencias.xml
Sirva una imagen del explorador de archivos del DDMS como prueba:
Si descargamos este fichero desde el DDMS y lo abrimos con cualquier editor de texto
veremos un contenido como el siguiente:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="nombre">prueba</string>
<string name="email">modificado@email.com</string>
</map>
En este XML podemos observar cmo se han almacenado las dos preferencias de ejemplo
que insertamos anteriormente, con sus claves y valores correspondientes.
Y nada ms, as de fcil y prctico. Con esto hemos aprendido una forma sencilla de
almacenar determinadas opciones de nuestra aplicacin sin tener que recurrir para ello a
definir bases de datos SQLite, que aunque tampoco aaden mucha dificultad s que
requieren algo ms de trabajo por nuestra parte.
Se aporta una pequea aplicacin de ejemplo para este artculo que tan slo incluye dos
botones, el primero de ellos para guardar las preferencias tal como hemos descrito, y el
segundo para recuperarlas y mostrarlas en el log.
En una segunda parte de este tema dedicado a las preferencias veremos cmo Android nos
ofrece otra forma de gestionar estos datos, que se integra adems fcilmente con la interfaz
grfica necesaria para solicitar los datos al usuario.
Preferencias en Android II: PreferenceActivity
by Sgoliver on 13/10/2011 in Android, Programacin
Ya hemos visto durante el curso algn artculo dedicado a las preferencias
compartidas (shared preferences), un mecanismo que nos permite gestionar fcilmente las
opciones de una aplicacin permitindonos guardarlas en XML de una forma transparente
para el programador. En aquel momento slo vimos cmo hacer uso de ellas mediante
204
Si atendemos por ejemplo a la primera imagen vemos cmo las diferentes opciones se
organizan dentro de la pantalla de opciones en varias categoras (General Settings y
Slideshow Settings). Dentro de cada categora pueden aparecer varias opciones de
diversos tipos, como por ejemplo de tipo checkbox (Confirm deletions) o de tipo lista de
seleccin (Display size). He resaltado las palabras pantalla de opciones, categoras, y
tipos de opcin porque sern estos los tres elementos principales con los que vamos a
definir el conjunto de opciones o preferencias de nuestra aplicacin. Empecemos.
Como hemos indicado, nuestra pantalla de opciones la vamos a definir mediante un XML,
de forma similar a como definimos cualquier layout, aunque en este caso deberemos
colocarlo en la carpeta /res/xml. El contenedor principal de nuestra pantalla de preferencias
ser el elemento <PreferenceScreen>. Este elemento representar a la pantalla de opciones
en s, dentro de la cual incluiremos el resto de elementos. Dentro de ste podremos incluir
205
opciones, las cuales pueden ser de distintos tipos, entre los que destacan:
CheckBoxPreference. Marca seleccionable.
EditTextPreference. Cadena simple de texto.
ListPreference. Lista de valores seleccionables (exclusiva).
MultiSelectListPreference. Lista de valores seleccionables (mltiple).
Cada uno de estos tipos de preferencia requiere la definicin de diferentes atributos, que
iremos viendo en los siguientes apartados.
CheckBoxPreference
Representa un tipo de opcin que slo puede tomar dos valores distintos: activada o
desactivada. Es el equivalente a un control de tipo checkbox. En este caso tan slo
tendremos que especificar los atributos: nombre interno de la opcin (android:key), texto a
mostrar (android:title) y descripcin de la opcin (android:summary). Veamos un ejemplo:
<CheckBoxPreference
android:key="opcion1"
android:title="Preferencia 1"
android:summary="Descripcin de la preferencia 1" />
EditTextPreference
Representa un tipo de opcin que puede contener como valor una cadena de texto. Al pulsar
sobre una opcin de este tipo se mostrar un cuadro de dilogo sencillo que solicitar al
usuario el texto a almacenar. Para este tipo, adems de los tres atributos comunes a todas
las opciones (key, title y summary) tambin tendremos que indicar el texto a mostrar en el
cuadro de dilogo, mediante el atributoandroid:dialogTitle. Un ejemplo sera el siguiente:
<EditTextPreference
android:key="opcion2"
android:title="Preferencia 2"
android:summary="Descripcin de la preferencia 2"
android:dialogTitle="Introduce valor" />
ListPreference
Representa un tipo de opcin que puede tomar como valor un elemento, y slo uno,
seleccionado por el usuario entre una lista de valores predefinida. Al pulsar sobre una
opcin de este tipo se mostrar la lista de valores posibles y el usuario podr seleccionar
uno de ellos. Y en este caso seguimos aadiendo atributos. Adems de los cuatro ya
comentados (key, title, summary y dialogTitle) tendremos que aadir dos ms, uno de ellos
206
indicando la lista de valores a visualizar en la lista y el otro indicando los valores internos
que utilizaremos para cada uno de los valores de la lista anterior (Ejemplo: al usuario
podemos mostrar una lista con los valores Espaol y Francs, pero internamente
almacenarlos como ESP y FRA).
Estas listas de valores las definiremos tambin como ficheros XML dentro de la
carpeta /res/xml. Definiremos para ello los recursos de tipos <string-array> necesarios, en
este caso dos, uno para la lista de valores visibles y otro para la lista de valores internos,
cada uno de ellos con su ID nico correspondiente. Veamos cmo quedaran dos listas de
ejemplo, en un fichero llamado codigospaises.xml:
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<string-array name="pais">
<item>Espaa</item>
<item>Francia</item>
<item>Alemania</item>
</string-array>
<string-array name="codigopais">
<item>ESP</item>
<item>FRA</item>
<item>ALE</item>
</string-array>
</resources>
En la preferencia utilizaremos los atributos android:entries y android:entryValues para
hacer referencia a estas listas, como vemos en el ejemplo siguiente:
<ListPreference
android:key="opcion3"
android:title="Preferencia 3"
android:summary="Descripcin de la preferencia 3"
android:dialogTitle="Indicar Pais"
android:entries="@array/pais"
android:entryValues="@array/codigopais" />
MultiSelectListPreference
[A partir de Android 3.0.x / Honeycomb] Las opciones de este tipo son muy similares a las
ListPreference, con la diferencia de que el usuario puede seleccionar varias de las opciones
de la lista de posibles valores. Los atributos a asignar son por tanto los mismos que para el
tipo anterior.
<MultiSelectListPreference
android:key="opcion4"
android:title="Preferencia 4"
android:summary="Descripcin de la preferencia 4"
android:dialogTitle="Indicar Pais"
207
android:entries="@array/pais"
android:entryValues="@array/codigopais" />
Como ejemplo completo, veamos cmo quedara definida una pantalla de opciones con las
3 primeras opciones comentadas (ya que probar con Android 2.2), divididas en 2
categoras llamadas por simplicidad Categora 1 y Categora 2. Llamaremos al fichero
opciones.xml.
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Categora 1">
<CheckBoxPreference
android:key="opcion1"
android:title="Preferencia 1"
android:summary="Descripcin de la preferencia 1" />
<EditTextPreference
android:key="opcion2"
android:title="Preferencia 2"
android:summary="Descripcin de la preferencia 2"
android:dialogTitle="Introduce valor" />
</PreferenceCategory>
<PreferenceCategory android:title="Categora 2">
<ListPreference
android:key="opcion3"
android:title="Preferencia 3"
android:summary="Descripcin de la preferencia 3"
android:dialogTitle="Indicar Pais"
android:entries="@array/pais"
android:entryValues="@array/codigopais" />
</PreferenceCategory>
</PreferenceScreen>
Ya tenemos definida la estructura de nuestra pantalla de opciones, pero an nos queda un
paso ms para poder hacer uso de ella desde nuestra aplicacin. Adems de la definicin
XML de la lista de opciones, debemos implementar una nueva actividad, que ser a la que
hagamos referencia cuando queramos mostrar nuestra pantalla de opciones y la que se
encargar internamente de gestionar todas las opciones, guardarlas, modificarlas, etc, a
partir de nuestra definicin XML.
Android nos facilita las cosas ofrecindonos una clase de la que podemos derivar
facilmente la nuestra propia y que hace casi todo el trabajo por nosotros. Esta clase se
llama PreferenceActivity. Tan slo deberemos crear una nueva actividad (yo la he
llamado OpcionesActivity)
que
extienda
esta
clase,
implementar
su
208
209
Sea cual se la opcin elegida para definir la pantalla de preferencias, el siguiente paso ser
aadir esta actividad al fichero AndroidManifest.xml, al igual que cualquier otra actividad
que utilicemos en la aplicacin.
<activity android:name=".OpcionesActivity"
android:label="@string/app_name">
</activity>
Ya slo nos queda aadir a nuestra aplicacin algn mecanismo para mostrar la pantalla de
preferencias. Esta opcin suele estar en un men (para Android 2.x) o en el men de
overflow de la action bar (para Android 3 o superior), pero por simplificar el ejemplo
vamos a aadir simplemente un botn (btnPreferencias) que abra la ventana de
preferencias.
Al pulsar este botn llamaremos a la ventana de preferencias mediante el
mtodo startActivity(), como ya hemos visto en alguna ocasin, al que pasaremos como
parmetros el contexto de la aplicacin (nos vale con nuestra actividad principal) y la clase
de la ventana de preferencias (OpcionesActivity.class).
btnPreferencias = (Button)findViewById(R.id.BtnPreferencias);
btnPreferencias.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this,
OpcionesActivity.class));
}
});
Y esto es todo, ya slo nos queda ejecutar la aplicacin en el emulador y pulsar el botn de
preferencias para mostrar nuestra nueva pantalla de opciones. Debe quedar como muestran
las imgenes siguientes para Android 2.x y 4.x respectivamente:
210
211
Por ltimo, la opcin 3 de tipo lista, nos mostrar una ventana emergente con la lista de
valores posibles, donde podremos seleccionar slo uno de ellos.
Una vez establecidos los valores de las preferencias podemos salir de la ventana de
opciones simplemente pulsando el botn Atrs del dispositivo o del emulador. Nuestra
actividad OpcionesActivity se habr ocupado por nosotros de guardar correctamente los
valores de las opciones haciendo uso de la API de preferencias compartidas (Shared
Preferences). Y para comprobarlo vamos a aadir otro botn (btnObtenerOpciones) a la
212
aplicacin de ejemplo que recupere el valor actual de las 3 preferencias y los escriba en el
log de la aplicacin.
La forma de acceder a las preferencias compartidas de la aplicacin ya la vimos en
el artculo anteriorsobre este tema. Obtenemos la lista de preferencias mediante el
mtodogetDefaultSharedPreferences() y posteriormente utilizamos
los distintos
mtodos get() para recuperar el valor de cada opcin dependiendo de su tipo.
btnObtenerPreferencias.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences pref =
PreferenceManager.getDefaultSharedPreferences(
AndroidPrefScreensActivity.this);
Log.i("", "Opcin 1: " + pref.getBoolean("opcion1", false));
Log.i("", "Opcin 2: " + pref.getString("opcion2", ""));
Log.i("", "Opcin 3: " + pref.getString("opcion3", ""));
}
});
Si ejecutamos ahora la aplicacin, establecemos las preferencias y pulsamos el nuevo botn
de consulta que hemos creado veremos cmo en el log de la aplicacin aparecen los valores
correctos de cada preferencia. Se mostrara algo como lo siguiente:
10-08 09:27:09.681: INFO/(1162): Opcin 1: true
10-08 09:27:09.681: INFO/(1162): Opcin 2: prueba
10-08 09:27:09.693: INFO/(1162): Opcin 3: FRA
Y hasta aqu hemos llegado con el tema de las preferencias, un tema muy interesante de
controlar ya que casi ninguna aplicacin se libra de hacer uso de ellas. Existen otras muchas
opciones de configuracin de las pantallas de preferencias, sobre todo con la llegada de
Android 4, pero con lo que hemos visto aqu podremos cubrir la gran mayora de casos.
213
214
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
public class UsuariosSQLiteHelper extends SQLiteOpenHelper {
//Sentencia SQL para crear la tabla de Usuarios
String sqlCreate = "CREATE TABLE Usuarios (codigo INTEGER, nombre TEXT)";
public UsuariosSQLiteHelper(Context contexto, String nombre,
CursorFactory factory, int version) {
super(contexto, nombre, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
//Se ejecuta la sentencia SQL de creacin de la tabla
db.execSQL(sqlCreate);
}
@Override
public void onUpgrade(SQLiteDatabase db, int versionAnterior, int versionNueva) {
//NOTA: Por simplicidad del ejemplo aqu utilizamos directamente la opcin de
//
eliminar la tabla anterior y crearla de nuevo vaca con el nuevo formato.
//
Sin embargo lo normal ser que haya que migrar datos de la tabla antigua
//
a la nueva, por lo que este mtodo debera ser ms elaborado.
//Se elimina la versin anterior de la tabla
db.execSQL("DROP TABLE IF EXISTS Usuarios");
//Se crea la nueva versin de la tabla
db.execSQL(sqlCreate);
}
}
Lo primero que hacemos es definir una variable llamado sqlCreate donde almacenamos la
sentencia SQL para crear una tabla llamada Usuarios con los campos
alfanumricos nombre e email. NOTA: No es objetivo de este tutorial describir la sintaxis
del lenguaje SQL ni las particularidades del motor de base de datos SQLite, por lo que no
entrar a describir las sentencias SQL utilizadas. Para ms informacin sobre SQLite
puedes consultar la documentacin oficial o empezar por leer una pequea introduccin que
hice en este mismo blog cuando trat el tema de utilizar SQLite desde aplicaciones .NET
El
mtodo onCreate() ser
ejecutado
automticamente
por
nuestra
clase UsuariosDBHelper cuando sea necesaria la creacin de la base de datos, es decir,
cuando an no exista. Las tareas tpicas que deben hacerse en este mtodo sern la creacin
215
de todas las tablas necesarias y la insercin de los datos iniciales si son necesarios. En
nuestro caso, slo vamos a crear la tabla Usuarios descrita anteriomente. Para la creacin de
la tabla utilizaremos la sentencia SQL ya definida y la ejecutaremos contra la base de datos
utilizando el mtodo ms sencillo de los disponibles en la API de SQLite proporcionada
por Android, llamado execSQL(). Este mtodo se limita a ejecutar directamente el cdigo
SQL que le pasemos como parmetro.
Por su parte, el mtodo onUpgrade() se lanzar automticamente cuando sea necesaria una
actualizacin de la estructura de la base de datos o una conversin de los datos. Un ejemplo
prctico: imaginemos que publicamos una aplicacin que utiliza una tabla con los
campos usuario e email (llammoslo versin 1 de la base de datos). Ms adelante,
ampliamos la funcionalidad de nuestra aplicacin y necesitamos que la tabla tambin
incluya un campo adicional como por ejemplo con la edad del usuario (versin 2 de nuestra
base de datos). Pues bien, para que todo funcione correctamente, la primera vez que
ejecutemos la versin ampliada de la aplicacin necesitaremos modificar la estructura de la
tabla Usuarios para aadir el nuevo campo edad. Pues este tipo de cosas son las que se
encargar de hacer automticamente el mtodo onUpgrade() cuando intentemos abrir una
versin concreta de la base de datos que an no exista. Para ello, como parmetros recibe la
versin actual de la base de datos en el sistema, y la nueva versin a la que se quiere
convertir. En funcin de esta pareja de datos necesitaremos realizar unas acciones u otras.
En nuestro caso de ejemplo optamos por la opcin ms sencilla: borrar la tabla actual y
volver a crearla con la nueva estructura, pero como se indica en los comentarios del cdigo,
lo habitual ser que necesitemos algo ms de lgica para convertir la base de datos de una
versin a otra y por supuesto para conservar los datos registrados hasta el momento.
Una vez definida nuestra clase helper, la apertura de la base de datos desde nuestra
aplicacin resulta ser algo de lo ms sencillo. Lo primero ser crear un objeto de la
clase UsuariosSQLiteHelper al que pasaremos el contexto de la aplicacin (en el ejemplo
216
217
36
37
38
39
40
Vale, y ahora qu? dnde est la base de datos que acabamos de crear? cmo podemos
comprobar que todo ha ido bien y que los registros se han insertado correctamente?
Vayamos por partes.
En primer lugar veamos dnde se ha creado nuestra base de datos. Todas las bases de datos
SQLite creadas por aplicaciones Android utilizando este mtodo se almacenan en la
memoria del telfono en un fichero con el mismo nombre de la base de datos situado en una
ruta que sigue el siguiente patrn:
/data/data/paquete.java.de.la.aplicacion/databases/nombre_base_datos
En el caso de nuestro ejemplo, la base de datos se almacenara por tanto en la ruta
siguiente:
/data/data/net.sgoliver.android.bd/databases/DBUsuarios
Para comprobar esto podemos hacer lo siguiente. Una vez ejecutada por primera vez desde
Eclipse la aplicacin de ejemplo sobre el emulador de Android (y por supuesto antes de
cerrarlo) podemos ir a la perspectiva DDMS (Dalvik Debug Monitor Server) de Eclipse y
en la solapa File Explorer podremos acceder al sistema de archivos del emulador, donde
podremos buscar la ruta indicada de la base de datos. Podemos ver esto en la siguiente
imagen (click para ampliar):
Con esto ya comprobamos al menos que el fichero de nuestra base de datos se ha creado en
la ruta correcta. Ya slo nos queda comprobar que tanto las tablas creadas como los datos
insertados tambin se han incluido correctamente en la base de datos. Para ello podemos
recurrir a dos posibles mtodos:
1. Trasnferir la base de datos a nuestro PC y consultarla con cualquier administrador de bases
de datos SQLite.
218
219
Con esto ya hemos comprobado que nuestra base de datos se ha creado correctamente, que
se han insertado todos los registros de ejemplo y que todo funciona segn se espera.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
En los siguientes artculos comentaremos las distintas posibilidades que tenemos a la hora
de manipular los datos de la base de datos (insertar, eliminar y modificar datos) y cmo
podemos realizar consultas sobre los mismos, ya que [como siempre] tendremos varias
opciones disponibles.
Bases de Datos en Android (II): Insertar/Actualizar/Eliminar
by Sgoliver on 03/02/2011 in Android, Programacin
En el artculo anterior del curso de programacin en Android vimos cmo crear una base de
datos para utilizarla desde nuestra aplicacin Android. En este segundo artculo de la serie
vamos a describir las posibles alternativas que proporciona la API de Android a la hora de
insertar, actualizar y eliminar registros de nuestra base de datos SQLite.
La API de SQLite de Android proporciona dos alternativas para realizar operaciones sobre
la
base
de
datos
que
no
devuelven
resultados
(entre
ellas
la
insercin/actualizacin/eliminacin de registros, pero tambin la creacin de tablas, de
ndices, etc).
El primero de ellos, que ya comentamos brevemente en el artculo anterior, es el
mtodo execSQL() de la clase SQLiteDatabase. Este mtodo permite ejecutar cualquier
sentencia SQL sobre la base de datos, siempre que sta no devuelva resultados. Para ello,
simplemente aportaremos como parmetro de entrada de este mtodo la cadena de texto
correspondiente con la sentencia SQL. Cuando creamos la base de datos en el post
anterior ya vimos algn ejemplo de esto para insertar los registros de prueba. Otros
ejemplos podran ser los siguientes:
//Insertar un registro
1
db.execSQL("INSERT INTO Usuarios (codigo,nombre) VALUES (6,'usuariopru')
2
");
3
4
//Eliminar un registro
5
db.execSQL("DELETE FROM Usuarios WHERE codigo=6 ");
6
7
//Actualizar un registro
8
db.execSQL("UPDATE Usuarios SET nombre='usunuevo' WHERE codigo=6 ");
220
221
Como vemos, volvemos a pasar como primer parmetro el nombre de la tabla y en segundo
lugar la condicin WHERE. Por supuesto, si no necesitramos ninguna condicin,
podramos dejar como null en este parmetro (lo que eliminara todos los registros de la
tabla).
Un ltimo detalle sobre estos mtodos. Tanto en el caso de execSQL() como en los casos
de update() odelete() podemos utilizar argumentos dentro de las condiones de la sentencia
SQL. stos no son ms que partes variables de la sentencia SQL que aportaremos en un
array de valores aparte, lo que nos evitar pasar por la situacin tpica en la que tenemos
que construir una sentencia SQL concatenando cadenas de texto y variables para formar el
comando SQL final. Estos argumentos SQL se indicarn con el smbolo ?, y los valores
de dichos argumentos deben pasarse en el array en el mismo orden que aparecen en la
sentencia SQL. As, por ejemplo, podemos escribir instrucciones como la siguiente:
1
//Eliminar un registro con execSQL(), utilizando argumentos
2
String[] args = new String[]{"usuario1"};
3
db.execSQL("DELETE FROM Usuarios WHERE nombre=?", args);
4
5
//Actualizar dos registros con update(), utilizando argumentos
6
ContentValues valores = new ContentValues();
7
valores.put("nombre","usunuevo");
8
9
String[] args = new String[]{"usuario1", "usuario2"};
10 db.update("Usuarios", valores, "nombre=? OR nombre=?", args);
Esta forma de pasar a la sentencia SQL determinados datos variables puede ayudarnos
adems a escribir cdigo ms limpio y evitar posibles errores.
Para este apartado he continuado con la aplicacin de ejemplo del apartado anterior, a la
que he aadido dos cuadros de texto para poder introducir el cdigo y nombre de un usuario
y tres botones para insertar, actualizar o eliminar dicha informacin.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
222
En el siguiente artculo veremos cmo consultar la base de datos para recuperar registros
segn un determinado criterio.
Bases de Datos en Android (III): Consultar/Recuperar registros
by Sgoliver on 07/02/2011 in Android, Programacin
En el anterior artculo del curso vimos todas las opciones disponibles a la hora de insertar,
actualizar y eliminar datos de una base de datos SQLite en Android. En esta nueva entrega
vamos a describir la ltima de las tareas importantes de tratamiento de datos que nos queda
por ver, la seleccin y recuperacin de datos.
De forma anloga a lo que vimos para las sentencias de modificacin de datos, vamos a
tener dos opciones principales para recuperar registros de una base de datos SQLite en
Android. La primera de ellas utilizando directamente un comando de seleccin SQL, y
como segunda opcin utilizando un mtodo especfico donde parametrizaremos la consulta
a la base de datos.
Para la primera opcin utilizaremos el mtodo rawQuery() de la clase SQLiteDatabase.
Este mtodo recibe directamente como parmetro un comando SQL completo, donde
indicamos los campos a recuperar y los criterios de seleccin. El resultado de la consulta lo
obtendremos en forma de cursor, que posteriormente podremos recorrer para procesar los
registros recuperados. Sirva la siguiente consulta a modo de ejemplo:
Cursor c = db.rawQuery(" SELECT codigo,nombre FROM Usuarios WHERE
1
nombre='usu1' ", null);
Como en el caso de los mtodos de modificacin de datos, tambin podemos aadir a este
mtodo una lista de argumentos variables que hayamos indicado en el comando SQL con el
smbolo ?, por ejemplo as:
String[] args = new String[] {"usu1"};
1
Cursor c = db.rawQuery(" SELECT codigo,nombre FROM Usuarios WHERE
2
nombre=? ", args);
Ms adelante en este artculo veremos cmo podemos manipular el objeto Cursor para
recuperar los datos obtenidos.
Como segunda opcin para recuperar datos podemos utilizar el mtodo query() de la
claseSQLiteDatabase. Este mtodo recibe varios parmetros: el nombre de la tabla, un array
con los nombre de campos a recuperar, la clusula WHERE, un array con los argumentos
variables incluidos en el WHERE(si los hay, null en caso contrario), la clusula GROUP
BY si existe, la clusula HAVING si existe, y por ltimo la clusula ORDER BY si existe.
Opcionalmente, se puede incluir un parmetro al final ms indicando el nmero mximo de
223
registros que queremos que nos devuelva la consulta. Veamos el mismo ejemplo anterior
utilizando el mtodo query():
1 String[] campos = new String[] {"codigo", "nombre"};
2 String[] args = new String[] {"usu1"};
3
4 Cursor c = db.query("Usuarios", campos, "usuario=?", args, null, null, null);
Como vemos, los resultados se devuelven nuevamente en un objeto Cursor que deberemos
recorrer para procesar los datos obtenidos.
Para recorrer y manipular el cursor devuelto por cualquiera de los dos mtodos
mencionados tenemos a nuestra disposicin varios mtodos de la clase Cursor, entre los que
destacamos dos de los dedicados a recorrer el cursor de forma secuencial y en orden
natural:
moveToFirst(): mueve el puntero del cursor al primer registro devuelto.
moveToNext(): mueve el puntero del cursor al siguiente registro devuelto.
Los mtodos moveToFirst() y moveToNext() devuelven TRUE en caso de haber realizado
el movimiento correspondiente del puntero sin errores, es decir, siempre que exista un
primer registro o un registro siguiente, respectivamente.
Una vez posicionados en cada registro podremos utilizar cualquiera de los
mtodosgetXXX(ndice_columna) existentes para cada tipo de dato para recuperar el dato
de cada campo del registro actual del cursor. As, si queremos recuperar por ejemplo la
segunda columna del registro actual, y sta contiene un campo alfanumrico, haremos la
llamada getString(1) [NOTA: los ndices comienzan por 0 (cero), por lo que la segunda
columna tiene ndice 1], en caso de contener un dato de tipo real llamaramos
a getDouble(1), y de forma anloga para todos los tipos de datos existentes.
Con todo esto en cuenta, veamos cmo podramos recorrer el cursor devuelto por el
ejemplo anterior:
1
2
3
4
5
6
7
8
9
10
11
12
13
224
Adems de los mtodos comentados de la clase Cursor existen muchos ms que nos pueden
ser tiles en muchas ocasiones. Por ejemplo, getCount() te dir el nmero total de registros
devueltos en el cursor,getColumnName(i) devuelve el nombre de la columna con ndice
i, moveToPosition(i) mueve el puntero del cursor al registro con ndice i, etc. Podis
consultar la lista completa de mtodos disponibles en la clase Cursor en la documentacin
oficial de Android.
En este apartado he seguido ampliando la aplicacin de ejemplo anterior para aadir la
posibilidad de recuperar todos los registros de la tabla Usuarios pulsando un nuevo botn
de consulta.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
Con esto, terminamos la serie de artculos bsicos dedicados a las tareas de mantenimiento
de datos en aplicaciones Android mediante bases de datos SQLite. Soy consciente de que
dejamos en el tintero algunos temas algo ms avanzados (como por ejemplo el uso
de transacciones, que intentar tratar ms adelante), pero con los mtodos descritos
podremos realizar un porcentaje bastante alto de todas las tareas necesarias relativas al
tratamiento de datos estructurados en aplicaciones Android.
Ficheros en Android
Ficheros en Android (I): Memoria Interna
by Sgoliver on 05/07/2011 in Android, Programacin
En artculos anteriores del Curso de Programacin Android hemos visto ya diversos
mtodos para almacenar datos en nuestras aplicaciones, como por ejemplo los ficheros
de preferencias compartidas o las bases de datos SQLite. Estos mecanismos son perfectos
para almacenar datos estructurados, pero en ocasiones nos seguir siendo til poder
225
disponer tambin de otros ficheros auxiliares de datos, probblemente con otro tipo de
contenidos y formatos. Por ello, en Android tambin podremos manipular ficheros
tradicionales de una forma muy similar a como se realiza en Java.
Lo primero que hay que tener en cuenta es dnde queremos almacenar los ficheros y el tipo
de acceso que queremos tener a ellos. As, podremos leer y escribir ficheros localizados en:
1. La memoria interna del dispositivo.
2. La tarjeta SD externa, si existe.
3. La propia aplicacin, en forma de recurso.
En los dos prximos artculos aprenderemos a manipular ficheros almacenados en
cualquiera de estos lugares, comentando las particularidades de cada caso.
Veamos en primer lugar cmo trabajar con la memoria interna del dispositivo. Cuando
almacenamos ficheros en la memoria interna debemos tener en cuenta las limitaciones de
espacio que tienen muchos dispositivos, por lo que no deberamos abusar de este espacio
utilizando ficheros de gran tamao.
Escribir ficheros en la memoria interna es muy sencillo. Android proporciona para ello el
mtodoopenFileOutput(), que recibe como parmetros el nombre del fichero y el modo de
acceso con el que queremos abrir el fichero. Este modo de acceso puede variar
entre MODE_PRIVATE para acceso privado desde nuestra aplicacin (crea el fichero o lo
sobrescribe si ya existe), MODE_APPEND para aadir datos a un fichero ya
existente, MODE_WORLD_READABLE para permitir a otras aplicaciones leer el fichero,
oMODE_WORLD_WRITABLE para permitir a otras aplicaciones escribir sobre el
fichero. Los dos ltimos no deberan utilizarse dada su peligrosidad, de hecho, han sido
declarados como obsoletos (deprecated) en la API 17.
Este mtodo devuelve una referencia al stream de salida asociado al fichero (en forma de
objetoFileOutputStream), a partir del cual ya podremos utilizar los mtodos de
manipulacin de ficheros tradicionales del lenguaje java (api java.io). Como ejemplo,
convertiremos este stream a unOutputStreamWriter para escribir una cadena de texto al
fichero.
1
try
2
{
3
OutputStreamWriter fout=
4
new OutputStreamWriter(
5
openFileOutput("prueba_int.txt", Context.MODE_PRIVATE));
6
7
fout.write("Texto de prueba.");
8
fout.close();
226
9
10
11
12
13
}
catch (Exception ex)
{
Log.e("Ficheros", "Error al escribir fichero a memoria interna");
}
Est bien, ya hemos creado un fichero de texto en la memoria interna, pero dnde
exactamente? Tal como ocurra con las bases de datos SQLite, Android almacena por
defecto los ficheros creados en una ruta determinada, que en este caso seguir el siguiente
patrn:
/data/data/paquete.java/files/nombre_fichero
En mi caso particular, la ruta ser
/data/data/net.sgoliver.android.ficheros/files/prueba_int.txt
Si ejecutamos el cdigo anterior podremos comprobar en el DDMS cmo el fichero se crea
correctamente en la ruta indicada (Al final del artculo hay un enlace a una aplicacin de
ejemplo sencilla donde incluyo un botn por cada uno de los puntos que vamos a comentar
en el artculo).
Por otra parte, leer ficheros desde la memoria interna es igual de sencillo, y procederemos
de forma anloga, con la nica diferencia de que utilizaremos el
mtodo openFileInput() para abrir el fichero, y los mtodos de lectura de java.io para leer el
contenido.
1
try
2
{
3
BufferedReader fin =
4
new BufferedReader(
5
new InputStreamReader(
6
openFileInput("prueba_int.txt")));
7
8
String texto = fin.readLine();
9
fin.close();
10 }
11 catch (Exception ex)
12 {
227
13
Log.e("Ficheros", "Error al leer fichero desde memoria interna");
14 }
La segunda forma de almacenar ficheros en la memoria interna del dispositivo es incluirlos
como recursoen la propia aplicacin. Aunque este mtodo es til en muchos casos, slo
debemos utilizarlo cuando no necesitemos realizar modificaciones sobre los ficheros, ya
que tendremos limitado el acceso a slo lectura.
Para incluir un fichero como recurso de la aplicacin debemos colocarlo en la carpeta
/res/raw de nuestro proyecto de Eclipse. Esta carpeta no suele estar creada por defecto,
por lo que deberemos crearla manualmente en Eclipse desde el men contextual con la
opcin New / Folder.
Una vez creada la carpeta /raw podremos colocar en ella cualquier fichero que queramos
que se incluya con la aplicacin en tiempo de compilacin en forma de recurso. Nosotros
incluiremos como ejemplo un fichero de texto llamado prueba_raw.txt. Ya en tiempo de
ejecucin podremos acceder a este fichero, slo en modo de lectura, de una forma similar a
la que ya hemos visto para el resto de ficheros en memoria interna.
Para acceder al fichero, accederemos en primer lugar a los recursos de la aplicacin con el
mtodogetResources() y
sobre
stos
utilizaremos
el
mtodo openRawResource(id_del_recurso) para abrir el fichero en modo lectura. Este
mtodo devuelve un objeto InputStream, que ya podremos manipular como queramos
mediante los mtodos de la API java.io. Como ejemplo, nosotros convertiremos el stream
en un objeto BufferedReader para leer el texto contenido en el fichero de ejemplo (por
supuesto los ficheros de recurso tambin pueden ser binarios, como por ejemplo ficheros de
imagen, video, etc). Veamos cmo quedara el cdigo:
1
try
2
{
3
InputStream fraw =
4
getResources().openRawResource(R.raw.prueba_raw);
5
6
BufferedReader brin =
7
new BufferedReader(new InputStreamReader(fraw));
8
228
9
String linea = brin.readLine();
10
11
fraw.close();
12 }
13 catch (Exception ex)
14 {
15
Log.e("Ficheros", "Error al leer fichero desde recurso raw");
16 }
Como puede verse en el cdigo anterior, al mtodo openRawResource() le pasamos como
parmetro el ID del fichero incluido como recurso, que seguir el patrn
R.raw.nombre_del_fichero, por lo que en nuestro caso particular ser R.raw.prueba_raw.
La aplicacin de ejemplo de este apartado consiste en una pantalla sencilla con 3 botones
que realizan exactamente las tres tareas que hemos comentado: escribir y leer un fichero en
la memoria interna, y leer un fichero de la carpeta raw.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
Para no alargar mucho el artculo, dejamos la gestin de ficheros en la memoria externa
para el prximo artculo.
Ficheros en Android (II): Memoria Externa (Tarjeta SD)
by Sgoliver on 06/07/2011 in Android, Programacin
En el artculo anterior del curso hemos visto cmo manipular ficheros localizados en la
memoria interna de un dispositivo Android. Sin embargo, como ya indicamos, esta
memoria suele ser relativamente limitada y no es aconsejable almacenar en ella ficheros de
gran tamao. La alternativa natural es utilizar para ello la memoria externa del dispositivo,
constituida normalmente por una tarjeta de memoria SD, aunque en dispositivos recientes
tambin est presente en forma de almacenamiento no extrable del dispositivo, aunque no
por ello debe confundirse con la memoria interna. A diferencia de la memoria interna, el
almacenamiento externo es pblico, es decir, todo lo que escribamos en l podr ser ledo
por otras aplicaciones y por el usuario, por tanto hay que tener cierto cuidado a la hora de
dicidir lo que escribimos en memoria interna y externa.
Una nota rpida antes de empezar con este tema. Para poder probar aplicaciones que hagan
uso de la memoria externa en el emulador de Android necesitamos tener configurado en
Eclipse un AVD que tenga establecido correctamente el tamao de la tarjeta SD. En mi
caso, he definido por ejemplo un tamao de tarjeta de 50 Mb:
229
puede leer y escribir en ella. Este mtodo devuelve una serie de valores que nos indicarn el
estado de la memoria externa, siendo los ms importantes los siguientes:
MEDIA_MOUNTED, que indica que la memoria externa est disponible y podemos tanto
leer como escribir en ella.
MEDIA_MOUNTED_READ_ONLY, que indica que la memoria externa est disponible
pero slo podemos leer de ella.
Otra serie de valores que indicarn que existe algn problema y que por tanto no podemos
ni leer ni escribir en la memoria externa (MEDIA_UNMOUNTED, MEDIA_REMOVED,
). Podis consultar todos estos estados en la documentacin oficial de la clase
Environment.
Con todo esto en cuenta, podramos realizar un chequeo previo del estado de la memoria
externa del dispositivo de la siguiente forma:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
230
18
19
20
21
{
sdDisponible = false;
sdAccesoEscritura = false;
}
Una vez chequeado el estado de la memoria externa, y dependiendo del resultado obtenido,
ya podremos leer o escribir en ella cualquier tipo de fichero.
Empecemos por la escritura. Para escribir un fichero a la memoria externa tenemos que
obtener en primer lugar la ruta al directorio raz de esta memoria. Para ello podemos
utilizar el mtodogetExternalStorageDirectory() de la clase Environment, que nos
devolver un objeto File con la ruta de dicho directorio. A partir de este objeto, podremos
construir otro con el nombre elegido para nuestro fichero (como ejemplo prueba_sd.txt),
creando un nuevo objeto File que combine ambos elementos. Tras esto, ya slo queda
encapsularlo en algn objeto de escritura de ficheros de la API de java y escribir algn dato
de prueba. En nuestro caso de ejemplo lo convertiremos una vez ms a un
objetoOutputStreamWriter para escribir al fichero un mensaje de texto. Veamos cmo
quedara el cdigo:
1
try
2
{
3
File ruta_sd = Environment.getExternalStorageDirectory();
4
5
File f = new File(ruta_sd.getAbsolutePath(), "prueba_sd.txt");
6
7
OutputStreamWriter fout =
8
new OutputStreamWriter(
9
new FileOutputStream(f));
10
11
fout.write("Texto de prueba.");
12
fout.close();
13 }
14 catch (Exception ex)
15 {
16
Log.e("Ficheros", "Error al escribir fichero a tarjeta SD");
17 }
El cdigo anterior funciona sin problemas pero escribir el fichero directamente en la
carpeta raz de la memoria externa. Esto, aunque en ocasiones puede resultar necesario, no
es una buena prctica. Lo correcto sera disponer de una carpeta propia para nuestra
aplicacin, lo que adems tendr la ventaja de que al desinstalar la aplicacin tambin se
liberar este espacio. Esto lo conseguimos utilizando el mtodogetExternalFilesDir(null) en
vez
de getExternalStorageDirectory().
El
mtodogetExternalFilesDir() nos
devuelve
231
directamente la ruta de una carpeta especfica para nuestra aplicacin dentro de la memoria
externa siguiendo el siguiente patrn:
<raz_mem_ext>/Android/data/nuestro.paquete.java/files
Si en vez de null le indicamos como parmetro un tipo de datos determinado
(DIRECTORY_MUSIC,DIRECTORY_PICTURES, DIRECTORY_MOVIES, DIRECTO
RY_RINGTONES, DIRECTORY_ALARMS,DIRECTORY_NOTIFICATIONS, DIRECT
ORY_PODCASTS) nos devolver una subcarpeta dentro de la anterior con su nombre
correspondiente.
As,
por
ejemplo,
una
llamada
al
mtodo
getExternalFilesDir(Environment.DIRECTORY_MUSIC) nos devolvera la siguiente
carpeta:
<raz_mem_ext>/Android/data/nuestro.paquete.java/files/Music
Esto ltimo, adems, ayuda a Android a saber qu tipo de contenidos hay en cada carpeta,
de forma que puedan clasificarse correctamente por ejemplo en la galera multimedia.
Sea como sea, para tener acceso a la memoria externa tendremos que especificar en el
ficheroAndroidManifest.xml que nuestra aplicacin necesita permiso de escritura en dicha
memoria. Para aadir un nuevo permiso usaremos como siempre la clusula <usespermission> utilizando
el
valor
android.permission.WRITE_EXTERNAL_STORAGE.
Con
nuestro AndroidManifest.xml quedara de forma similar a ste:
1
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
package="net.sgoliver.android.ficheros"
3
android:versionCode="1"
4
android:versionName="1.0" >
5
6
<uses-sdk
7
android:minSdkVersion="8"
8
android:targetSdkVersion="17" />
9
10
<uses-permission
11 android:name="android.permission.WRITE_EXTERNAL_STORAGE">
12
</uses-permission>
13
14
<application
15
android:allowBackup="true"
16
android:icon="@drawable/ic_launcher"
17
android:label="@string/app_name"
18
android:theme="@style/AppTheme" >
19
<activity
20
android:name="net.sgoliver.android.ficheros.MainActivity"
21
android:label="@string/app_name" >
concreto
esto,
232
22
23
24
25
26
27
28
29
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Si ejecutamos ahora el cdigo y nos vamos al explorador de archivos del DDMS podremos
comprobar cmose ha creado correctamente el fichero en el directorio raiz de nuestra SD.
Esta ruta puede variar entre dispositivos, pero para Android 2.x suele localizarse en la
carpeta /sd-card/, mientras que para Android 4 suele estar en /mnt/sdcard/.
Por su parte, leer un fichero desde la memoria externa es igual de sencillo. Obtenemos el
directorio raiz de la memoria externa con getExternalStorageDirectory(), o la carpeta
especfica de nuestra aplicacin con getExternalFilesDir() como ya hemos visto, creamos
un objeto File que combine esa ruta con el nombre del fichero a leer y lo encapsulamos
dentro de algn objeto que facilite la lectura lectura, nosotros para leer texto utilizaremos
como siempre un BufferedReader.
1
try
2
{
3
File ruta_sd = Environment.getExternalStorageDirectory();
4
5
File f = new File(ruta_sd.getAbsolutePath(), "prueba_sd.txt");
6
7
BufferedReader fin =
8
new BufferedReader(
9
new InputStreamReader(
10
new FileInputStream(f)));
11
12
String texto = fin.readLine();
13
fin.close();
14 }
15 catch (Exception ex)
233
16
17
18
{
Log.e("Ficheros", "Error al leer fichero desde tarjeta SD");
}
Como vemos, el cdigo es anlogo al que hemos visto para la escritura de ficheros.
Como aplicacin de ejemplo de este artculo he partido de la desarrollada en el artculo
anterior dedicado a la memoria interna y he aadido dos nuevos botones para leer y escribir
a memoria externa tal como hemos descrito. Los resultados se muestran en el log de la
aplicacin.
234
y una versin anloga del tercero (XmlPull). Por supuesto con cualquiera de los tres
modelos podemos hacer las mismas tareas, pero ya veremos cmo dependiendo de la
naturaleza de la tarea que queramos realizar va a resultar ms eficiente utilizar un modelo u
otro.
Antes de empezar, unas anotaciones respecto a los ejemplos que voy a utilizar. Estas
tcnicas se pueden utilizar para tratar cualquier documento XML, tanto online como local,
pero por utilizar algo conocido por la mayora de vosotros todos los ejemplos van a trabajar
sobre los datos XML de un documento RSS online, y en mi caso utilizar como ejemplo
el canal RSS de portada de europapress.com.
Un documento RSS de este feed tiene la estructura siguiente:
1
<rss version="2.0">
2
<channel>
3
<title>Europa Press</title>
4
<link>http://www.europapress.es/</link>
5
<description>Noticias de Portada.</description>
6
<image>
7
<url>http://s01.europapress.net/eplogo.gif</url>
8
<title>Europa Press</title>
9
<link>http://www.europapress.es</link>
10
</image>
11
<language>es-ES</language>
12
<copyright>Copyright</copyright>
13
<pubDate>Sat, 25 Dec 2010 23:27:26 GMT</pubDate>
14
<lastBuildDate>Sat, 25 Dec 2010 22:47:14 GMT</lastBuildDate>
15
<item>
16
<title>Ttulo de la noticia 1</title>
17
<link>http://link_de_la_noticia_2.es</link>
18
<description>Descripcin de la noticia 2</description>
19
<guid>http://identificador_de_la_noticia_2.es</guid>
20
<pubDate>Fecha de publicacin 2</pubDate>
21
</item>
22
<item>
23
<title>Ttulo de la noticia 2</title>
24
<link>http://link_de_la_noticia_2.es</link>
25
<description>Descripcin de la noticia 2</description>
26
<guid>http://identificador_de_la_noticia_2.es</guid>
27
<pubDate>Fecha de publicacin 2</pubDate>
28
</item>
29
...
30
</channel>
31 </rss>
235
236
36
public void setDescripcion(String d) {
37
descripcion = d;
38
}
39
40
public void setGuid(String g) {
41
guid = g;
42
}
43
44
public void setFecha(String f) {
45
fecha = f;
46
}
47 }
Una vez conocemos la estructura del XML a leer y hemos definido las clases auxiliares que
nos hacen falta para almacenar los datos, pasamos ya a comentar el primero de los modelos
de tratamiento de XML.
SAX en Android
En el modelo SAX, el tratamiento de un XML se basa en un analizador (parser) que a
medida que lee secuencialmente el documento XML va generando diferentes eventos con la
informacin de cada elemento leido. Asi, por ejemplo, a medida que lee el XML, si
encuentra el comienzo de una etiqueta <title>generar un evento de comienzo de
etiqueta, startElement(), con su informacin asociada, si despus de esa etiqueta encuentra
un fragmento de texto generar un evento characters() con toda la informacin necesaria, y
as sucesivamente hasta el final del documento. Nuestro trabajo consistir por tanto en
implementar las acciones necesarias a ejecutar para cada uno de los eventos posibles que se
pueden generar durante la lectura del documento XML.
Los principales eventos que se pueden producir son los siguientes (consultar aqu la lista
completa):
startDocument(): comienza el documento XML.
endDocument(): termina el documento XML.
startElement(): comienza una etiqueta XML.
endElement(): termina una etiqueta XML.
characters(): fragmento de texto.
Todos estos mtodos estn definidos en la clase org.xml.sax.helpers.DefaultHandler, de la
cual deberemos derivar una clase propia donde se sobrescriban los eventos necesarios. En
nuestro caso vamos a llamarla RssHandler.
1
public class RssHandler extends DefaultHandler {
2
private List<Noticia> noticias;
3
private Noticia noticiaActual;
4
private StringBuilder sbTexto;
5
237
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
238
53
54
55
56
57
58
59
60
61
62
63
64
65
}
@Override
public void startElement(String uri, String localName,
String name, Attributes attributes) throws SAXException {
super.startElement(uri, localName, name, attributes);
if (localName.equals("item")) {
noticiaActual = new Noticia();
}
}
}
Como se puede observar en el cdigo de anterior, lo primero que haremos ser incluir como
miembro de la clase la lista de noticias que pretendemos construir, List<Noticia> noticias, y
un mtodogetNoticias() que permita obtenerla tras la lectura completa del documento. Tras
esto, implementamos directamente los eventos SAX necesarios.
Comencemos por startDocument(), este evento indica que se ha comenzado a leer el
documento XML, por lo que lo aprovecharemos para inicializar la lista de noticias y las
variables auxiliares.
Tras ste, el evento startElement() se lanza cada vez que se encuentra una nueva etiqueta de
apertura. En nuestro caso, la nica etiqueta que nos interesar ser <item>, momento en el
que inicializaremos un nuevo objeto auxiliar de tipo Noticia donde almacenaremos
posteriormente los datos de la noticia actual.
El siguiente evento relevante es characters(), que se lanza cada vez que se encuentra un
fragmento de texto en el interior de una etiqueta. La tcnica aqu ser ir acumulando en una
variable auxiliar, sbTexto, todos los fragmentos de texto que encontremos hasta detectarse
una etiqueta de cierre.
Por ltimo, en el evento de cierre de etiqueta, endElement(), lo que haremos ser almacenar
en el atributo apropiado del objeto noticiaActual (que conoceremos por el
parmetro localName devuelto por el evento) el texto que hemos ido acumulando en la
variable sbTexto y limpiaremos el contenido de dicha variable para comenzar a acumular el
siguiente dato. El nico caso especial ser cuando detectemos el cierre de la
etiqueta <item>, que significar que hemos terminado de leer todos los datos de la noticia y
por tanto aprovecharemos para aadir la noticia actual a la lista de noticias que estamos
construyendo.
Una vez implementado nuestro handler, vamos a crear una nueva clase que haga uso de l
para parsear mediante SAX un documento XML concreto. A esta clase la llamaremos
RssParserSax. Ms adelante crearemos otras clases anlogas a sta que hagan lo mismo
239
pero utilizando los otros dos mtodos de tratamiento de XML ya mencionados. Esta clase
tendr nicamente un constructor que reciba como parmetro la URL del documento a
parsear, y un mtodo pblico llamado parse() para ejecutar la lectura del documento, y que
devolver como resultado una lista de noticias. Veamos cmo queda esta clase:
1
import java.io.IOException;
2
import java.io.InputStream;
3
import java.util.List;
4
5
import java.net.URL;
6
import javax.xml.parsers.SAXParser;
7
import java.net.MalformedURLException;
8
import javax.xml.parsers.SAXParserFactory;
9
10 public class RssParserSax
11 {
12
private URL rssUrl;
13
14
public RssParserSax(String url)
15
{
16
try
17
{
18
this.rssUrl = new URL(url);
19
}
20
catch (MalformedURLException e)
21
{
22
throw new RuntimeException(e);
23
}
24
}
25
26
public List<Noticia> parse()
27
{
28
SAXParserFactory factory = SAXParserFactory.newInstance();
29
30
try
31
{
32
SAXParser parser = factory.newSAXParser();
33
RssHandler handler = new RssHandler();
34
parser.parse(this.getInputStream(), handler);
35
return handler.getNoticias();
36
}
37
catch (Exception e)
38
{
39
throw new RuntimeException(e);
40
}
41
}
240
42
43
44
45
46
47
48
49
50
51
52
53
54
241
11
12
13
14
15
16
17
18
19
20
21
Las lineas 6 y 9 del cdigo anterior son las que hacen toda la magia. Primero creamos el
parser SAX pasndole la URL del documento XML y posteriormente llamamos al
mtodo parse() para obtener una lista de objetos de tipo Noticia que posteriormente
podremos manipular de la forma que queramos. As de sencillo.
Adicionalmente, para que este ejemplo funcione debemos aadir previamente permisos de
acceso a internet para la aplicacin. Esto se hace en el fichero AndroidManifest.xml, que
quedara de la siguiente forma:
1
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
package="net.sgoliver"
3
android:versionCode="1"
4
android:versionName="1.0">
5
6
<uses-permission
7
android:name="android.permission.INTERNET" />
8
9
<application android:icon="@drawable/icon"
10
android:label="@string/app_name">
11
<activity android:name=".AndroidXml"
12
android:label="@string/app_name">
13
<intent-filter>
14
<action android:name="android.intent.action.MAIN" />
15
<category android:name="android.intent.category.LAUNCHER" />
16
</intent-filter>
17
</activity>
18
19
</application>
20
<uses-sdk android:minSdkVersion="7" />
21 </manifest>
En la linea 6 del cdigo podis ver cmo aadimos el permiso de acceso a la red mediante
el elemento<uses-permission> con el parmetro android.permission.INTERNET
Pero hay algo ms, si ejecutamos el cdigo anterior en una versin de Android anterior a la
3.0 no tendremos ningn problema. La aplicacin descargar el XML y lo parsear tal
242
como hemos definido. Sin embargo, si utilizamos una versin de Android 3.0 o superior
nos encontraremos con algo similar a esto:
243
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
}
protected void onPostExecute(Boolean result) {
//Tratamos la lista de noticias
//Por ejemplo: escribimos los ttulos en pantalla
txtResultado.setText("");
for(int i=0; i<noticias.size(); i++)
{
txtResultado.setText(
txtResultado.getText().toString() +
System.getProperty("line.separator") +
noticias.get(i).getTitulo());
}
}
}
Por ltimo, sustituiremos el cdigo de nuestra actividad principal por una simple llamada
para crear y ejecutar la tarea en segundo plano:
1
public void onCreate(Bundle savedInstanceState)
2
{
3
super.onCreate(savedInstanceState);
4
setContentView(R.layout.main);
5
6
//Carga del XML mediante la tarea asncrona
7
8
CargarXmlTask tarea = new CargarXmlTask();
9
tarea.execute("http://www.europapress.es/rss/rss.aspx");
10 }
Y ahora s. Con esta ligera modificacin del cdigo nuestra aplicacin se ejecutar
correctamente en cualquier versin de Android.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
En los siguientes artculos veremos un modelo SAX alternativo con algunas ventajas sobre
el que acabamos de comentar, y los otros dos mtodos de tratamiento XML en Android
indicados (DOM y StAX).
Tratamiento de XML en Android (II): SAX Simplificado
by Sgoliver on 19/01/2011 in Android, Programacin
En el artculo anterior del tutorial vimos cmo realizar la lectura y tratamiento de un
documento XML utilizando el modelo SAX clsico. Vimos cmo implementar
244
un handler SAX, donde se definan las acciones a realizar tras recibirse cada uno de los
posibles eventos generados por el parser XML.
Este modelo, a pesar de funcionar perfectamente y de forma bastante eficiente, tiene claras
desventajas. Por un lado se hace necesario definir una clase independiente para el handler.
Adicionalmente, la naturaleza del modelo SAX implica la necesidad de poner bastante
atencin a la hora de definir dichohandler, ya que los eventos SAX definidos no estan
ligados de ninguna forma a etiquetas concretas del documento XML sino que se lanzarn
para todas ellas, algo que obliga entre otras cosas a realizar la distincin entre etiquetas
dentro de cada evento y a realizar otros chequeos adicionales.
Estos problemas se pueden observar perfectamente en el evento endElement() que
definimos en el ejemplo del artculo anterior. En primer lugar tenamos que comprobar la
condicin de que el atributonoticiaActual no fuera null, para evitar confundir el
elemento <title> descendiente de <channel>con el del mismo nombre pero descendiente
de <item>. Posteriormente, tenamos que distinguir con un IF gigantesco entre todas las
etiquetas posibles para realizar una accin u otra. Y todo esto para un documento XML
bastante sencillo. No es dificil darse cuenta de que para un documento XML algo ms
elaborado la complejidad del handler podra dispararse rpidamente, dando lugar a posibles
errores.
Para evitar estos problemas, Android propone una variante del modelo SAX que evita
definir una clase separada para el handler y que permite asociar directamente las acciones a
etiquetas concretas dentro de la estructura del documento XML, lo que alivia en gran
medida los inconvenientes mencionados.
Veamos cmo queda nuestro parser XML utilizando esta variante simplificada de SAX
para Android y despus comentaremos los aspectos ms importantes del mismo.
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.xml.sax.Attributes;
import android.sax.Element;
import android.sax.EndElementListener;
import android.sax.EndTextElementListener;
import android.sax.RootElement;
245
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import android.sax.StartElementListener;
import android.util.Xml;
public class RssParserSax2
{
private URL rssUrl;
private Noticia noticiaActual;
public RssParserSax2(String url)
{
try
{
this.rssUrl = new URL(url);
}
catch (MalformedURLException e)
{
throw new RuntimeException(e);
}
}
public List<Noticia> parse()
{
final List<Noticia> noticias = new ArrayList<Noticia>();
RootElement root = new RootElement("rss");
Element channel = root.getChild("channel");
Element item = channel.getChild("item");
item.setStartElementListener(new StartElementListener(){
public void start(Attributes attrs) {
noticiaActual = new Noticia();
}
});
item.setEndElementListener(new EndElementListener(){
public void end() {
noticias.add(noticiaActual);
}
});
item.getChild("title").setEndTextElementListener(
new EndTextElementListener(){
public void end(String body) {
noticiaActual.setTitulo(body);
}
});
246
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
item.getChild("link").setEndTextElementListener(
new EndTextElementListener(){
public void end(String body) {
noticiaActual.setLink(body);
}
});
item.getChild("description").setEndTextElementListener(
new EndTextElementListener(){
public void end(String body) {
noticiaActual.setDescripcion(body);
}
});
item.getChild("guid").setEndTextElementListener(
new EndTextElementListener(){
public void end(String body) {
noticiaActual.setGuid(body);
}
});
item.getChild("pubDate").setEndTextElementListener(
new EndTextElementListener(){
public void end(String body) {
noticiaActual.setFecha(body);
}
});
try
{
Xml.parse(this.getInputStream(),
Xml.Encoding.UTF_8,
root.getContentHandler());
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
return noticias;
}
private InputStream getInputStream()
{
try
{
return rssUrl.openConnection().getInputStream();
247
108
109
110
111
112
113
114
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
}
248
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
En el siguiente artculo pasaremos ya a describir el siguiente de los mtodos de lectura de
XML en Android, llamado Document Object Model (DOM).
Tratamiento de XML en Android (III): DOM
by Sgoliver on 24/01/2011 in Android, Programacin
En el artculo anterior del curso de programacin para Android hablamos sobre SAX, el
primero de los mtodos disponibles en Android para leer ficheros XML desde nuestras
aplicaciones. En este segundo artculo vamos a centrarnos en DOM, otro de los mtodos
clsicos para la lectura y tratamiento de XML.
Cuando comentbamos la filosofa de SAX ya vimos cmo con dicho modelo el
tratamiento del fichero XML se realizaba de forma secuencial, es decir, se iban realizando
las acciones necesarias durante la propia lectura del documento. Sin embargo, con DOM la
estrategia cambia radicalmente. Con DOM, el documento XML se lee completamente antes
de poder realizar ninguna accin en funcin de su contenido. Esto es posible gracias a que,
como resultado de la lectura del documento, el parser DOM devuelve todo su contenido en
forma de una estructura de tipo rbol, donde los distintos elementos del XML se representa
en forma de nodos y su jerarqua padre-hijo se establece mediante relaciones entre dichos
nodos.
Como ejemplo, vemos un ejemplo de XML sencillo y cmo quedara su representacin en
forma de rbol:
1
<noticias>
2
<noticia>
3
<titulo>T1</titulo>
4
<link>L1</link>
5
</noticia>
6
<noticia>
7
<titulo>T2</titulo>
8
<link>L2</link>
9
</noticia>
10 <noticias>
Este XML se traducira en un rbol parecido al siguiente:
249
Como vemos, este rbol conserva la misma informacin contenida en el fichero XML pero
en forma de nodos y transiciones entre nodos, de forma que se puede navegar fcilmente
por la estructura. Adems, este rbol se conserva persistente en memoria una vez leido el
documento completo, lo que permite procesarlo en cualquier orden y tantas veces como sea
necesario (a diferencia de SAX, donde el tratamiento era secuencial y siempre de principio
a fin del documento, no pudiendo volver atrs una vez finalizada la lectura del XML).
Para todo esto, el modelo DOM ofrece una serie de clases y mtodos que permiten
almacenar la informacin de la forma descrita y facilitan la navegacin y el tratamiento de
la estructura creada.
Veamos cmo quedara nuestro parser utilizando el modelo DOM y justo despus
comentaremos los detalles ms importantes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
250
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
251
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
{
String texto = obtenerTexto(dato);
noticia.setDescripcion(texto);
}
else if (etiqueta.equals("guid"))
{
noticia.setGuid(dato.getFirstChild().getNodeValue());
}
else if (etiqueta.equals("pubDate"))
{
noticia.setFecha(dato.getFirstChild().getNodeValue());
}
}
noticias.add(noticia);
}
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
return noticias;
}
private String obtenerTexto(Node dato)
{
StringBuilder texto = new StringBuilder();
NodeList fragmentos = dato.getChildNodes();
for (int k=0;k<fragmentos.getLength();k++)
{
texto.append(fragmentos.item(k).getNodeValue());
}
return texto.toString();
}
private InputStream getInputStream()
{
try
{
return rssUrl.openConnection().getInputStream();
}
catch (IOException e)
{
throw new RuntimeException(e);
252
111
112
113
}
}
}
Nos centramos una vez ms en el mtodo parse(). Al igual que hacamos para SAX, el
primer paso ser instanciar una nueva fbrica, esta vez de tipo DocumentBuilderFactory, y
posteriormente crear un nuevo parser a partir de ella mediante el
mtodo newDocumentBuilder().
Tras esto, ya podemos realizar la lectura del documento XML llamando al mtod parse() de
nuestro parser DOM, pasndole como parmetro el stream de entrada del fichero. Al hacer
esto, el documento XML se leer completo y se generar la estructura de rbol equivalente,
que se devolver como un objeto de tipo Document. ste ser el objeto que podremos
navegar para realizar eltratamiento necesario del XML.
Para ello, lo primero que haremos ser acceder al nodo principal del rbol (en nuestro caso,
la etiqueta<rss>) utilizando el mtodo getDocumentElement(). Una vez posicionados en
dicho nodo, vamos a buscar todos los nodos cuya etiqueta sea <item>. Esto lo conseguimos
utilizando
el
mtodo
de
bsqueda
por
nombre
de
etiqueta, getElementsByTagName(nombre_de_etiqueta), que devolver una lista (de
tipo NodeList) con todos los nodos hijos del nodo actual cuya etiqueta coincida con la
pasada como parmetro.
Una vez tenemos localizados todos los elementos <item>, que representan a cada noticia,
los vamos a recorrer uno a uno para ir generando todos los objetos Noticia necesarios. Para
cada uno de ellos, se obtendrn los nodos hijos del elemento mediante getChildNodes() y se
recorrern stos obteniendo su texto y almacenndolo en el atributo correspondiente del
objeto Noticia. Para saber a qu etiqueta corresponde cada nodo hijo utilizamos el
mtodo getNodeName().
Merece la pena pararnos un poco en comentar la forma de obtener el texto contenido en un
nodo. Como vimos al principio del artculo en el ejemplo grfico de rbol DOM, el texto de
un nodo determinado se almacena a su vez como nodo hijo de dicho nodo. Este nodo de
texto suele ser nico, por lo que la forma habitual de obtener el texto de un nodo es obtener
su primer nodo hijo y de ste ltimo obtener su valor:
1
253
1
private String obtenerTexto(Node dato)
2
{
3
StringBuilder texto = new StringBuilder();
4
NodeList fragmentos = dato.getChildNodes();
5
6
for (int k=0;k<fragmentos.getLength();k++)
7
{
8
texto.append(fragmentos.item(k).getNodeValue());
9
}
10
11
return texto.toString();
12 }
Como vemos, el modelo DOM nos permite localizar y tratar determinados elementos
concretos del documento XML, sin la necesidad de recorrer todo su contenido de principio
a fin. Adems, a diferencia de SAX, como tenemos cargado en memoria el documento
completo de forma persistente (en forma de objetoDocument), podremos consultar, recorrer
y tratar el documento tantas veces como sea necesario sin necesidad de volverlo a parsear.
En un artculo posterior veremos como todas estas caractersticas pueden ser ventajas o
inconvenientes segn el contexto de la aplicacin y el tipo de XML tratado.
Tratamiento de XML en Android (IV): XmlPull
by Sgoliver on 25/01/2011 in Android, Programacin
En los artculos anteriores dedicados al tratamiento de XML en aplicaciones Android (parte
1, parte 2, parte 3) dentro de nuestro tutorial de programacin Android hemos comentado
ya los modelos SAX y DOM, los dos mtodos ms comunes de lectura de XML soportados
en la plataforma.
En este cuarto artculo nos vamos a centrar en el ltimo mtodo menos conocido, aunque
igual de vlido segn el contexto de la aplicacin, llamado XmlPull. Este mtodo es una
versin similar al modelo StAX(Streaming API for XML), que en esencia es muy parecido
al modelo SAX ya comentado. Y digo muy parecido porque tambin se basa en definir las
acciones a realizar para cada uno de los eventos generados durante la lectura secuencial del
documento XML. Cul es la diferencia entonces?
La diferencia radica principalmente en que, mientras que en SAX no tenamos control sobre
la lectura del XML una vez iniciada (el parser lee automticamente el XML de principio a
fin generando todos los eventos necesarios), en el modelo XmlPull vamos a poder guiar o
intervenir en la lectura del documento, siendo nosotros los que vayamos pidiendo de forma
explcita la lectura del siguiente elemento del XML y respondiendo al resultado ejecutando
las acciones oportunas.
254
255
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
if (etiqueta.equals("item"))
{
noticiaActual = new Noticia();
}
else if (noticiaActual != null)
{
if (etiqueta.equals("link"))
{
noticiaActual.setLink(parser.nextText());
}
else if (etiqueta.equals("description"))
{
noticiaActual.setDescripcion(parser.nextText());
}
else if (etiqueta.equals("pubDate"))
{
noticiaActual.setFecha(parser.nextText());
}
else if (etiqueta.equals("title"))
{
noticiaActual.setTitulo(parser.nextText());
}
else if (etiqueta.equals("guid"))
{
noticiaActual.setGuid(parser.nextText());
}
}
break;
case XmlPullParser.END_TAG:
etiqueta = parser.getName();
if (etiqueta.equals("item") && noticiaActual != null)
{
noticias.add(noticiaActual);
}
break;
}
evento = parser.next();
}
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
256
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
return noticias;
}
private InputStream getInputStream()
{
try
{
return rssUrl.openConnection().getInputStream();
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
}
Centrndonos una vez ms en el mtodo parse(), vemos que tras crear el nuevo parser
XmlPull y establecer el fichero de entrada en forma de stream
[mediante XmlPull.newPullParser() yparser.setInput()] nos metemos en un buble en el
que iremos solicitando al parser en cada paso el siguiente evento encontrado en la lectura
del XML, utilizando para ello el mtodo parser.next(). Para cada evento devuelto como
resultado consultaremos su tipo mediante el mtodo parser.getEventType()y responderemos
con
las
acciones
oportunas
segn
dicho
tipo
(START_DOCUMENT, END_DOCUMENT,START_TAG, END_TAG).
Una
vez
identificado el tipo concreto de evento, podremos consultar el nombre de la etiqueta del
elemento
XML
mediante parser.getName() y
su
texto
correspondiente
medianteparser.nextText(). En cuanto a la obtencin del texto, con este modelo tenemos la
ventaja de no tener que preocuparnos por recolectar todos los fragmentos de texto
contenidos en el elemento XML, ya quenextText() devolver todo el texto que encuentre
hasta el prximo evento de fin de etiqueta (ENT_TAG).
Y sobre este modelo de tratamiento no queda mucho ms que decir, ya que las acciones
ejecutadas en cada caso son anlogas a las que ya vimos en los artculos anteriores.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
Espero haber sido capaz de mostrar con claridad en estos cuatro artculos todas las
posibilidades existentes a la hora de leer y procesar documentos XML en aplicaciones
Android.
Alternativas para leer y escribir XML (y otros ficheros) en Android
257
258
de salida, asignar el fichero como salida del serializador y construir el XML mediante los
mtodos startTag(), text() yendTag() pasndoles los nombres de etiqueta y contenidos de
texto correspondientes (aunque existen ms mtodos, por ejemplo para escribir atributos de
una etiqueta, stos tres son los principales). Finalmente deberemos llamar
a endDocument() para finalizar y cerrar nuestro documento XML. Un ejemplo equivalente
al anterior utilizando este mtodo sera el siguiente:
1
//Creamos el serializer
2
XmlSerializer ser = Xml.newSerializer();
3
4
//Creamos un fichero en memoria interna
5
OutputStreamWriter fout =
6
new OutputStreamWriter(
7
openFileOutput("prueba_pull.xml",
8
Context.MODE_PRIVATE));
9
10 //Asignamos el resultado del serializer al fichero
11 ser.setOutput(fout);
12
13 //Construimos el XML
14 ser.startTag("", "usuario");
15
16 ser.startTag("", "nombre");
17 ser.text("Usuario1");
18 ser.endTag("", "nombre");
19
20 ser.startTag("", "apellidos");
21 ser.text("ApellidosUsuario1");
22 ser.endTag("", "apellidos");
23
24 ser.endTag("", "usuario");
25
26 ser.endDocument();
27
28 fout.close();
As de sencillo, no merece la pena complicarse la vida con otros mtodos ms complicados.
Leer ficheros XML en Android desde recursos locales
Para leer ficheros XML desde un recurso local se me ocurren varias posibilidades, por
ejemplo leerlo desde la memoria interna/externa del dispositivo, leer un XML desde un
recurso XML (carpeta /res/xml), desde un recurso Raw (carpeta /res/raw), o directamente
desde la carpeta /assets de nuestro proyecto. Salvo en el segundo caso (recurso XML), en
todos los dems la solucin va a pasar por conseguir una referencia de tipo InputStream (os
259
recuerdo que cualquiera de los mtodos que vimos para leer un XML partan de una
referencia de este tipo) a partir de nuestro fichero o recurso XML, sea cual sea su
localizacin.
As, por ejemplo, si el fichero XML est almacenado en la memoria interna de nuestro
dispositivo, podramos acceder a l mediante el mtodo openFileInput() tal como vimos en
los artculos dedicados a tratamiento de ficheros. Este mtodo devuelve un objeto de
tipo FileInputStream, que al derivar deInputStream podemos utilizarlo como entrada a
cualquiera de los mecanismos de lectura de XML (SAX,DOM, XmlPull).
1
//Obtenemos la referencia al fichero XML de entrada
2
FileInputStream fil = openFileInput("prueba.xml");
3
4
//DOM (Por ejemplo)
5
DocumentBuilderFactory factory =
6
DocumentBuilderFactory.newInstance();
7
8
DocumentBuilder builder = factory.newDocumentBuilder();
9
Document dom = builder.parse(fil);
10
11 //A partir de aqu se tratara el rbol DOM como siempre.
12 //Por ejemplo:
13 Element root = dom.getDocumentElement();
14
15 //...
En el caso de encontrarse el fichero como recurso Raw, es decir, en la carpeta /res/raw,
tendramos
que
obtener
la
referencia
al
fichero
mediante
el
mtodo getRawResource() pasndole como parmetro el ID de recurso del fichero. Esto
nos devuelve directamente el stream de entrada en forma de InputStream.
1
//Obtenemos la referencia al fichero XML de entrada
2
InputStream is = getResources().openRawResource(R.raw.prueba);
3
4
//DOM (Por ejemplo)
5
DocumentBuilderFactory factory =
6
DocumentBuilderFactory.newInstance();
7
8
DocumentBuilder builder = factory.newDocumentBuilder();
9
Document dom = builder.parse(is);
10
11 //A partir de aqu se tratara el rbol DOM como siempre.
12 //Por ejemplo:
13 Element root = dom.getDocumentElement();
14
15 //...
260
261
262
263
ello, debemos pasarle el nombre del provider que queremos consultar. Para los ms
comunes tenemos varias constantes ya definidas:
LocationManager.NETWORK_PROVIDER. Localizacin por la red de telefona.
LocationManager.GPS_PROVIDER. Localizacin por GPS.
De esta forma, si quisiramos saber si el GPS est habilitado o no en el dispositivo (y
actuar en consecuencia), haramos algo parecido a lo siguiente:
1
2
3
4
264
proveedor que se le indique como parmetro. Y esta posicin se pudo obtener hace pocos
segundos, hace das, hace meses, o incluso nunca (si el dispositivo ha estado apagado, si
nunca se ha activado el GPS, ). Por tanto, cuidado cuando se haga uso de la posicin
devuelta por el mtodo getLastKnownLocation().
Entonces, de qu forma podemos obtener la posicin real actualizada? Pues la forma
correcta de proceder va a consistir en algo as como activar el proveedor de localizacin y
suscribirnos a sus notificaciones de cambio de posicin. O dicho de otra forma, vamos a
suscribirnos al evento que se lanza cada vez que un proveedor recibe nuevos datos sobre la
localizacin actual. Y para ello, vamos a darle previamente unas indicaciones (que no
ordenes, ya veremos esto en el prximo artculo) sobre cada cuanto tiempo o cada cuanta
distacia recorrida necesitaramos tener una actualizacin de la posicin.
Todo esto lo vamos a realizar mediante una llamada al mtodo requestLocationUpdates(),
En cuanto al listener, ste ser del tipo LocationListener y contendr una serie de mtodos
asociados a los distintos eventos que podemos recibir del proveedor:
onLocationChanged(location). Lanzado cada vez que se recibe una actualizacin de la
posicin.
onProviderDisabled(provider). Lanzado cuando el proveedor se deshabilita.
onProviderEnabled(provider). Lanzado cuando el proveedor se habilita.
onStatusChanged(provider, status, extras). Lanzado cada vez que el proveedor cambia su
estado,
que
puede
variar
entre OUT_OF_SERVICE, TEMPORARILY_UNAVAILABLE, AVAILABLE.
Por nuestra parte, tendremos que implementar cada uno de estos mtodos para responder a
los eventos del proveedor, sobre todo al ms interesante, onLocationChanged(), que se
265
ejecutar cada vez que se recibe una nueva localizacin desde el proveedor. Veamos un
ejemplo de cmo implementar un listener de este tipo:
1
LocationListener locListener = new LocationListener() {
2
3
public void onLocationChanged(Location location) {
4
mostrarPosicion(location);
5
}
6
7
public void onProviderDisabled(String provider){
8
lblEstado.setText("Provider OFF");
9
}
10
11
public void onProviderEnabled(String provider){
12
lblEstado.setText("Provider ON");
13
}
14
15
public void onStatusChanged(String provider, int status, Bundle extras){
16
lblEstado.setText("Provider Status: " + status);
17
}
18 };
Como podis ver, en nuestro caso de ejemplo nos limitamos a mostrar al usuario la
informacin recibida en el evento, bien sea un simple cambio de estado, o una nueva
posicin, en cuyo caso llamamos al mtodo auxiliar mostrarPosicion() para refrescar todos
los datos de la posicin en la pantalla. Para este ejemplo hemos construido una interfaz muy
sencilla, donde se muestran 3 datos de la posicin (latitud, longitud y precisin) y un campo
para mostrar el estado del proveedor. Adems, se incluyen dos botones para comenzar y
detener la recepcin de nuevas actualizaciones de la posicin. No incluyo aqu el cdigo de
la interfaz para no alargar ms el artculo, pero puede consultarse en el cdigo
fuente suministrado al final del texto. El aspecto de nuestra ventana es el siguiente:
266
267
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
268
269
15
locationListener = new LocationListener() {
16
public void onLocationChanged(Location location) {
17
muestraPosicion(location);
18
}
19
public void onProviderDisabled(String provider){
20
lblEstado.setText("Provider OFF");
21
}
22
public void onProviderEnabled(String provider){
23
lblEstado.setText("Provider ON");
24
}
25
public void onStatusChanged(String provider, int status, Bundle extras){
26
Log.i("LocAndroid", "Provider Status: " + status);
27
lblEstado.setText("Provider Status: " + status);
28
}
29
};
30
31
locationManager.requestLocationUpdates(
32
LocationManager.GPS_PROVIDER, 15000, 0, locationListener);
33 }
34
35 private void muestraPosicion(Location loc) {
36
if(loc != null)
37
{
38
lblLatitud.setText("Latitud: " + String.valueOf(loc.getLatitude()));
39
lblLongitud.setText("Longitud: " + String.valueOf(loc.getLongitude()));
40
lblPrecision.setText("Precision: " + String.valueOf(loc.getAccuracy()));
41
Log.i("LocAndroid", String.valueOf(
42
loc.getLatitude() + " - " + String.valueOf(loc.getLongitude())));
43
}
44
else
45
{
46
lblLatitud.setText("Latitud: (sin_datos)");
47
lblLongitud.setText("Longitud: (sin_datos)");
48
lblPrecision.setText("Precision: (sin_datos)");
49
}
50 }
Si ejecutamos en este momento la aplicacin en el emulador y pulsamos el
botn Activar veremos cmo los cuadros de texto se rellenan con la informacin de la
ltima posicin conocida (si existe), pero sin embargo estos datos no cambiarn en ningn
momento ya que por el momento el emulador de Android tan slo cuenta con esa
informacin. Cmo podemos simular la actualizacin de la posicin del dispositivo para
ver si nuestra aplicacin responde exactamente como esperamos?
Pues bien, para hacer esto tenemos varias opciones. La primera de ellas, y la ms sencilla,
es el envo manual de una nueva posicin al emulador de Android, para simular que ste
270
Con estos controles podemos enviar de forma manual al emulador en ejecucin unas
nuevas coordenadas de posicin, para simular que stas se hubieran recibido a travs del
proveedor de localizacin utilizado. De esta forma, si introducimos unas coordenadas de
longitud y latitud y pulsamos el botn Send mientras nuestra aplicacin se ejecuta en el
emulador, esto provocar la ejecucin del eventoonLocationChanged() y por consiguiente
se mostrarn estos mismos datos en sus controles correspondientes de la interfaz, como
vemos en la siguiente captura de pantalla:
Por supuesto, si hacemos nuevos envos de coordenadas desde Eclipse veremos cmo sta
se va actualizando en nuestra aplicacin sin ningn tipo de problamas. Sin embargo este
271
272
Una vez cargado el fichero, tendremos disponibles los cuatro botones inferiores para (de
izquierda a derecha):
273
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
05-08
10:50:47.131:
INFO/LocAndroid(251):
7.000008333333333
11.999989999999999
05-08 10:50:47.182: INFO/LocAndroid(251): Provider Status: 1
05-08 10:51:02.232: INFO/LocAndroid(251): 7.000023333333333 - -11.999975
05-08
10:51:02.812:
INFO/LocAndroid(251):
7.000023333333333
11.999973333333333
05-08 10:51:02.872: INFO/LocAndroid(251): Provider Status: 2
05-08
10:51:03.872:
INFO/LocAndroid(251):
7.000024999999999
11.999973333333333
05-08
10:51:04.912:
INFO/LocAndroid(251):
7.000026666666668
11.999971666666665
05-08
10:51:05.922:
INFO/LocAndroid(251):
7.000026666666668
11.999971666666665
05-08 10:51:06.982: INFO/LocAndroid(251): 7.000028333333334 - -11.99997
05-08
10:51:08.032:
INFO/LocAndroid(251):
7.000028333333334
11.999968333333333
05-08 10:51:09.062: INFO/LocAndroid(251): 7.00003 - -11.999968333333333
05-08
10:51:10.132:
INFO/LocAndroid(251):
7.000031666666667
11.999966666666667
05-08
10:51:12.242:
INFO/LocAndroid(251):
7.000033333333333
11.999965000000001
05-08
10:51:13.292:
INFO/LocAndroid(251):
7.000033333333333
11.999963333333335
05-08 10:51:13.342: INFO/LocAndroid(251): Provider Status: 1
05-08
10:51:28.372:
INFO/LocAndroid(251):
7.000048333333333
11.999950000000002
05-08
10:51:28.982:
INFO/LocAndroid(251):
7.000048333333333
11.999950000000002
05-08 10:51:29.032: INFO/LocAndroid(251): Provider Status: 2
05-08
10:51:30.002:
INFO/LocAndroid(251):
7.000050000000001
11.999948333333334
05-08
10:51:31.002:
INFO/LocAndroid(251):
7.000051666666667
11.999946666666665
05-08
10:51:33.111:
INFO/LocAndroid(251):
7.000053333333333
11.999944999999999
05-08
10:51:34.151:
INFO/LocAndroid(251):
7.000053333333333
11.999944999999999
05-08 10:51:35.201: INFO/LocAndroid(251): 7.000055 - -11.999943333333333
05-08 10:51:36.251: INFO/LocAndroid(251): 7.0000566666666675 11.999941666666667
05-08 10:51:37.311: INFO/LocAndroid(251): 7.0000566666666675 11.999941666666667
05-08 10:51:38.361: INFO/LocAndroid(251): 7.0000583333333335 - -11.99994
05-08 10:51:38.431: INFO/LocAndroid(251): Provider Status: 1
Estudiemos un poco este log. Si observamos las marcas de fecha hora vemos varias cosas:
274
Un primer grupo de actualizaciones entre las 10:50:37 y las 10:50:47, con 8 lecturas.
Un segundo grupo de actualizaciones entre las 10:51:02 y las 10:51:13, con 11 lecturas.
Un tercer grupo de actualizaciones entre las 10:51:28 y las 10:51:38, con 10 lecturas.
Entre cada grupo de lecturas transcurren aproximadamente 15 segundos.
Los grupos estn formados por un nmero variable de lecturas.
Por tanto ya podemos sacar algunas conclusiones. Indicar al location listener una
frecuencia de 15 segundos entre actualizaciones no quiere decir que vayamos a tener una
sola lectura cada 15 segundos, sino que al menos tendremos una nueva con dicha
frecuencia. Sin embargo, como podemos comprobar en los logs, las lecturas se recibirn
por grupos separados entre s por el intervalo de tiempo indicado.
Ms conclusiones, ahora sobre el estado del proveedor de localizacin. Si buscamos en el
log los momentos donde cambia el estado del proveedor vemos dos cosas importantes:
275
276
Un primer detalle a tener en cuenta es que los registros de datos proporcionados por un
content provider deben contar siempre con un campo llamado _ID que los identifique de
forma unvoca del resto de registros. Como ejemplo, los registros devueltos por un content
provider de clientes podra tener este aspecto:
_ID
Cliente
Telefono
Antonio
900123456
email1@correo.com
Jose
900123123
email2@correo.com
Luis
900123987
email3@correo.com
Sabiendo esto, es interesante que nuestros datos tambin cuenten internamente con este
campo _ID (no tiene por qu llamarse igual) de forma que nos sea ms sencillo despus
generar los resultados del content provider.
Con todo esto, y para tener algo desde lo que partir, vamos a construir en primer lugar una
aplicacin de ejemplo muy sencilla con una base de datos SQLite que almacene los datos
de una serie de clientes con una estructura similar a la tabla anterior. Para ello seguiremos
los mismos pasos que ya comentamos en los artculos dedicados al tratamiento de bases de
datos SQLite en Android (consultar ndice del curso).
Por volver a recordarlo muy brevemente, lo que haremos ser crear una nueva clase que
extienda aSQLiteOpenHelper, definiremos las sentencias SQL para crear nuestra tabla de
clientes, e implementaremos finalmente los mtodos onCreate() y onUpgrade(). El cdigo
de esta nueva clase, que yo he llamado ClientesSqliteHelper, quedara como sigue:
1
public class ClientesSqliteHelper extends SQLiteOpenHelper {
2
3
//Sentencia SQL para crear la tabla de Clientes
4
String sqlCreate = "CREATE TABLE Clientes " +
5
"(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
6
" nombre TEXT, " +
7
" telefono TEXT, " +
8
" email TEXT )";
9
10
public ClientesSqliteHelper(Context contexto, String nombre,
11
CursorFactory factory, int version) {
12
13
super(contexto, nombre, factory, version);
14
}
277
15
16
@Override
17
public void onCreate(SQLiteDatabase db) {
18
//Se ejecuta la sentencia SQL de creacin de la tabla
19
db.execSQL(sqlCreate);
20
21
//Insertamos 15 clientes de ejemplo
22
for(int i=1; i<=15; i++)
23
{
24
//Generamos los datos de muestra
25
String nombre = "Cliente" + i;
26
String telefono = "900-123-00" + i;
27
String email = "email" + i + "@mail.com";
28
29
//Insertamos los datos en la tabla Clientes
30
db.execSQL("INSERT INTO Clientes (nombre, telefono, email) " +
31
"VALUES ('" + nombre + "', '" + telefono +"', '" + email + "')");
32
}
33
}
34
35
@Override
36
public void onUpgrade(SQLiteDatabase db, int versionAnterior, int versionNueva) {
37
//NOTA: Por simplicidad del ejemplo aqu utilizamos directamente la opcin de
38
//
eliminar la tabla anterior y crearla de nuevo vaca con el nuevo formato.
39
//
Sin embargo lo normal ser que haya que migrar datos de la tabla antigua
40
//
a la nueva, por lo que este mtodo debera ser ms elaborado.
41
42
//Se elimina la versin anterior de la tabla
43
db.execSQL("DROP TABLE IF EXISTS Clientes");
44
45
//Se crea la nueva versin de la tabla
46
db.execSQL(sqlCreate);
47
}
48 }
Como notas relevantes del cdigo anterior:
Ntese el campo _id que hemos incluido en la base de datos de clientes por lo motivos
indicados un poco ms arriba. Este campo lo declaramos como INTEGER PRIMARY KEY
AUTOINCREMENT, de forma que se incremente automticamente cada vez que
insertamos un nuevo registro en la base de datos.
En el mtodo onCreate(), adems de ejecutar la sentencia SQL para crear la tabla Clientes,
tambin inserta varios registros de ejemplo.
Para simplificar el ejemplo, el mtodo onUpgrade() se limita a eliminar la tabla actual y
crear una nueva con la nueva estructura. En una aplicacin real habra que hacer
probblemente la migracin de los datos a la nueva base de datos.
278
Dado que la clase anterior ya se ocupa de todo, incluso de insertar algunos registro de
ejemplo con los que podamos hacer pruebas, la aplicacin principal de ejemplo no mostrar
en principio nada en pantalla ni har nada con la informacin. Esto lo he decidido as para
no complicar el cdigo de la aplicacin innecesariamente, ya que no nos va a interesar el
tratamiento directo de los datos por parte de la aplicacin principal, sino su utilizacin a
travs del content provider que vamos a construir.
Una vez que ya contamos con nuestra aplicacin de ejemplo y su base de datos, es hora de
empezar a construir el nuevo content provider que nos permitir compartir sus datos con
otras aplicaciones.
Lo primero que vamos a comentar es la forma con que se hace referencia en Android a los
content providers. El acceso a un content provider se realiza siempre mediante una URI.
Una URI no es ms que una cadena de texto similar a cualquiera de las direcciones web que
utilizamos en nuestro navegador. Al igual que para acceder a mi blog lo hacemos mediante
la direccin http://www.sgoliver.net, para acceder a un content provider utilizaremos una
direccin similar a content://net.sgoliver.android.contentproviders/clientes.
Las direcciones URI de los content providers estn formadas por 3 partes. En primer lugar
el prefijo content:// que indica que dicho recurso deber ser tratado por un content
provider. En segundo lugar se indica el identificador en s del content provider, tambin
llamado authority. Dado que este dato debe ser nico es una buena prctica utilizar un
authority de tipo nombre de clase java invertido, por ejemplo en mi caso
net.sgoliver.android.contentproviders. Por ltimo, se indica la entidad concreta a la que
queremos acceder dentro de los datos que proporciona el content provider. En nuestro caso
ser simplemente la tabla de clientes, ya que ser la nica existente, pero dado que un
content provider puede contener los datos de varias entidades distintas en este ltimo tramo
de la URI habr que especificarlo. Indicar por ltimo que en una URI se puede hacer
referencia directamente a un registro concreto de la entidad seleccionada. Esto se hara
indicando al final de la URI el ID de dicho registro. Por ejemplo la uri
content://net.sgoliver.android.contentproviders/clientes/23 hara referencia directa al
cliente con _ID = 23.
Todo esto es importante ya que ser nuestro content provider el encargado de
interpretar/parsear la URI completa para determinar los datos que se le estn solicitando.
Esto lo veremos un poco ms adelante.
279
280
7
8
9
10
Por ltimo, vamos a definir varios atributos privados auxiliares para almacenar el nombre
de la base de datos, la versin, y la tabla a la que acceder nuestro content provider.
1
2
3
4
5
//Base de datos
private ClientesSqliteHelper clidbh;
private static final String BD_NOMBRE = "DBClientes";
private static final int BD_VERSION = 1;
private static final String TABLA_CLIENTES = "Clientes";
Como se indic anteriormente, la primera tarea que nuestro content provider deber hacer
cuando se acceda a l ser interpretar la URI utilizada. Para facilitar esta tarea Android
proporciona una clase llamada UriMatcher, capaz de interpretar determinados patrones en
una URI. Esto nos ser til para determinar por ejemplo si una URI hace referencia a una
tabla genrica o a un registro concreto a travs de su ID. Por ejemplo:
281
queremos identificar dicho formato. Ms tarde veremos cmo utilizar esto de forma
prctica.
Bien, pues ya tenemos definidos todos los miembros necesarios para nuestro nuevo content
provider. Ahora toca implementar los mtodos comentados anteriormente.
El primero de ellos es onCreate(). En este mtodo nos limitaremos simplemente a
inicializar nuestra base de datos, a travs de su nombre y versin, y utilizando para ello la
clase ClientesSqliteHelper que creamos al principio del artculo.
1 @Override
2 public boolean onCreate() {
3
4
clidbh = new ClientesSqliteHelper(
5
getContext(), BD_NOMBRE, null, BD_VERSION);
6
7
return true;
8 }
La parte interesante llega con el mtodo query(). Este mtodo recibe como parmetros una
URI, una lista de nombres de columna, un criterio de seleccin, una lista de valores para las
variables utilizadas en el criterio anterior, y un criterio de ordenacin. Todos estos datos
son anlogos a los que comentamos cuando tratamos la consulta de datos en SQLite para
Android, artculo que recomiendo releer si no tenis muy frescos estos conocimientos. El
mtodo query deber devolver los datos solicitados segn la URI indicada y los criterios de
seleccin y ordenacin pasados como parmetro. As, si la URI hace referencia a un cliente
concreto por su ID se deber ser el nico registro devuelto. Si por el contrario es un acceso
genrico a la tabla de clientes habr que realizar la consulta SQL correspondiente a la base
de datos respetanto los criterios pasados como parmetro.
Para disitinguir entre los dos tipos de URI posibles utilizaremos como ya hemos indicado el
objetouriMatcher, utilizando su mtodo match(). Si el tipo devuelto es CLIENTES_ID, es
decir, que se trata de un acceso a un cliente concreto, sustituiremos el criterio de seleccin
por uno que acceda a la tabla de clientes slo por el ID indicado en la URI. Para obtener
este ID utilizaremos el mtodogetLastPathSegment() del objeto uri que extrae el ltimo
elemento de la URI, en este caso el ID del cliente.
Hecho esto, ya tan slo queda realizar la consulta a la base de datos mediante el
mtodo query() deSQLiteDatabase. Esto es sencillo ya que los parmetros son anlogos a
los recibidos en el mtodoquery() del content provider.
1
@Override
2
public Cursor query(Uri uri, String[] projection,
3
String selection, String[] selectionArgs, String sortOrder) {
4
282
5
6
7
8
9
10
11
12
13
14
15
16
17
Como vemos, los resultados se devuelven en forma de Cursor, una vez ms exactamente
igual a como lo hace el mtodo query() de SQLiteDatabase.
Por su parte, los mtodos update() y delete() son completamente anlogos a ste, con la
nica diferencia de que devuelven el nmero de registros afectados en vez de un cursor a
los resultados. Vemos directamente el cdigo:
1
@Override
2
public int update(Uri uri, ContentValues values,
3
String selection, String[] selectionArgs) {
4
5
int cont;
6
7
//Si es una consulta a un ID concreto construimos el WHERE
8
String where = selection;
9
if(uriMatcher.match(uri) == CLIENTES_ID){
10
where = "_id=" + uri.getLastPathSegment();
11
}
12
13
SQLiteDatabase db = clidbh.getWritableDatabase();
14
15
cont = db.update(TABLA_CLIENTES, values, where, selectionArgs);
16
17
return cont;
18 }
19
20 @Override
21 public int delete(Uri uri, String selection, String[] selectionArgs) {
22
23
int cont;
24
25
//Si es una consulta a un ID concreto construimos el WHERE
26
String where = selection;
27
if(uriMatcher.match(uri) == CLIENTES_ID){
283
28
29
30
31
32
33
34
35
36
El mtodo insert() s es algo diferente, aunque igual de sencillo. La diferencia en este caso
radica en que debe devolver la URI que hace referencia al nuevo registro insertado. Para
ello, obtendremos el nuevo ID del elemento insertado como resultado del
mtodo insert() de SQLiteDatabase, y posteriormente construiremos la nueva URI mediante
el mtodo auxiliar ContentUris.withAppendedId() que recibe como parmetro la URI de
nuestro content provider y el ID del nuevo elemento.
1
@Override
2
public Uri insert(Uri uri, ContentValues values) {
3
4
long regId = 1;
5
6
SQLiteDatabase db = clidbh.getWritableDatabase();
7
8
regId = db.insert(TABLA_CLIENTES, null, values);
9
10
Uri newUri = ContentUris.withAppendedId(CONTENT_URI, regId);
11
12
return newUri;
13 }
Por ltimo, tan slo nos queda implementar el mtodo getType(). Este mtodo se utiliza
para identificar el tipo de datos que devuelve el content provider. Este tipo de datos se
expresar como un MIME Type, al igual que hacen los navegadores web para determinar el
tipo de datos que estn recibiendo tras una peticin a un servidor. Identificar el tipo de
datos que devuelve un content provider ayudar por ejemplo a Android a determinar qu
aplicaciones son capaces de procesar dichos datos.
Una vez ms existirn dos tipos MIME distintos para cada entidad del content provider,
uno de ellos destinado a cuando se devuelve una lista de registros como resultado, y otro
para cuando se devuelve un registro nico concreto. De esta forma, seguiremos los
siguientes patrones para definir uno y otro tipo de datos:
284
vnd.android.cursor.item/vnd.sgoliver.cliente
vnd.android.cursor.dir/vnd.sgoliver.cliente
Con esto en cuenta, la implementacin del mtodo getType() quedara como sigue:
1
@Override
2
public String getType(Uri uri) {
3
4
int match = uriMatcher.match(uri);
5
6
switch (match)
7
{
8
case CLIENTES:
9
return "vnd.android.cursor.dir/vnd.sgoliver.cliente";
10
case CLIENTES_ID:
11
return "vnd.android.cursor.item/vnd.sgoliver.cliente";
12
default:
13
return null;
14
}
15 }
Como se puede observar, utilizamos una vez ms el objeto UriMatcher para determinar el
tipo de URI que se est solicitando y en funcin de sta devolvemos un tipo MIME u otro.
Pues bien, con esto ya hemos completado la implementacin del nuevo content provider.
Pero an nos queda un paso ms, como indicamos al principio del artculo. Debemos
declarar el content provider en nuestro fichero AndroidManifest.xml de forma que una vez
instalada la aplicacin en el dispositivo Android conozca la existencia de dicho recurso.
Para ello, bastar insertar un nuevo elemento <provider> dentro de <application> indicando
el nombre del content provider y su authority.
1 <application android:icon="@drawable/icon"
2
android:label="@string/app_name">
3
4
...
5
6
<provider android:name="ClientesProvider"
7
android:authorities="net.sgoliver.android.contentproviders"/>
8
9 </application>
Ahora s hemos completado totalmente la construccin de nuestro nuevo content provider
mediante el cual otras aplicaciones del sistema podrn acceder a los datos almacenados por
nuestra aplicacin.
285
En el siguiente artculo veremos cmo utilizar este nuevo content provider para acceder a
los datos de nuestra aplicacin de ejemplo, y tambin veremos cmo podemos utilizar
alguno de los content provider predefinidos por Android para consultar datos del sistema,
como por ejemplo la lista de contactos o la lista de llamadas realizadas.
Content Providers en Android (II): Utilizacin
by Sgoliver on 31/08/2011 in Android, Programacin
En el artculo anterior del Curso de Programacin en Android vimos como construir
un content providerpersonalizado para permitir a nuestras aplicaciones Android compartir
datos con otras aplicaciones del sistema. En este nuevo artculo vamos a ver el tema desde
el punto de vista opuesto, es decir, vamos a aprender a hacer uso de un content provider ya
existente para acceder a datos de otras aplicaciones. Adems, veremos cmo tambin
podemos acceder a datos del propio sistema Android (logs de llamadas, lista de contactos,
agenda telefnica, bandeja de entrada de sms, etc) utilizando este mismo mecanismo.
Vamos a comenzar explicando cmo podemos utilizar el content provider que
implementamos en el artculo anterior para acceder a los datos de los clientes. Para no
complicar mucho el ejemplo ni hacer ms dificil las pruebas y la depuracin en el emulador
de Android vamos a hacer uso el content provider desde la propia aplicacin de ejemplo
que hemos creado. De cualquier forma, el cdigo necesario sera exactamente igual si lo
hiciramos desde otra aplicacin distinta.
Utilizar un content provider ya existente es muy sencillo, sobre todo comparado con el
laborioso proceso de construccin de uno nuevo. Para comenzar, debemos obtener una
referencia a un Content Resolver, objeto a travs del que realizaremos todas las acciones
necesarias sobre el content provider. Esto es tan fcil como utilizar el
mtodo getContentResolver() desde nuestra actividad para obtener la referencia indicada.
Una vez obtenida la referencia al content resolver, podremos utilizar sus
mtodos query(),update(), insert() y delete() para realizar las acciones equivalentes sobre
el content provider. Por ver varios ejemplos de la utilizacin de estos mtodos aadiremos
a nuestra aplicacin de ejemplo tres botones en la pantalla principal, uno para hacer una
consulta de todos los clientes, otro para insertar registros nuevos, y el ltimo para eliminar
todos los registros nuevos insertados con el segundo botn.
Empecemos por la consulta de clientes. El procedimiento ser prcticamente igual al que
vimosen los artculos de acceso a bases de datos SQLite (consultar el ndice del curso).
Comenzaremos por definir un array con los nombres de las columnas de la tabla que
286
queremos recuperar en los resultados de la consulta, que en nuestro caso sern el ID, el
nombre, el telfono y el email. Tras esto, obtendremos como dijimos antes una referencia
al content resolver y utilizaremos su mtodo query() para obtener los resultados en forma
de cursor. El mtodo query() recibe, como ya vimos en el artculo anterior, la Uri del
content provider al que queremos acceder, el array de columnas que queremos recuperar, el
criterio de seleccin, los argumentos variables, y el criterio de ordenacin de los resultados.
En nuestro caso, para no complicarnos utilizaremos tan slo los dos primeros, pasndole
el CONTENT_URI de nuestro provider y el array de columnas que acabamos de definir.
1
//Columnas de la tabla a recuperar
2
String[] projection = new String[] {
3
Clientes._ID,
4
Clientes.COL_NOMBRE,
5
Clientes.COL_TELEFONO,
6
Clientes.COL_EMAIL };
7
8
Uri clientesUri = ClientesProvider.CONTENT_URI;
9
10 ContentResolver cr = getContentResolver();
11
12 //Hacemos la consulta
13 Cursor cur = cr.query(clientesUri,
14
projection, //Columnas a devolver
15
null,
//Condicin de la query
16
null,
//Argumentos variables de la query
17
null);
//Orden de los resultados
Hecho esto, tendremos que recorrer el cursor para procesar los resultados. Para nuestro
ejemplo, simplemente los escribiremos en un cuadro de texto (txtResultados) colocado bajo
los tres botones de ejemplo. Una vez ms, si tienes dudas sobre cmo recorrer un cursor,
puedes consultar los artculos del curso dedicados al tratamiento de bases de datos SQLite,
por ejemplo ste. Veamos cmo quedara el cdigo:
1
if (cur.moveToFirst())
2
{
3
String nombre;
4
String telefono;
5
String email;
6
7
int colNombre = cur.getColumnIndex(Clientes.COL_NOMBRE);
8
int colTelefono = cur.getColumnIndex(Clientes.COL_TELEFONO);
9
int colEmail = cur.getColumnIndex(Clientes.COL_EMAIL);
10
11
txtResultados.setText("");
12
13
do
287
14
15
16
17
18
19
20
21
22
{
nombre = cur.getString(colNombre);
telefono = cur.getString(colTelefono);
email = cur.getString(colEmail);
txtResultados.append(nombre + " - " + telefono + " - " + email + "\n");
} while (cur.moveToNext());
}
Para insertar nuevos registros, el trabajo ser tambin exactamente igual al que se hace al
tratar directamente con bases de datos SQLite. Rellenaremos en primer lugar un objeto
ContentValues con los datos del nuevo cliente y posteriormente utilizamos el
mtodo insert() pasndole la URI del content provider en primer lugar, y los datos del
nuevo registro como segundo parmetro.
1 ContentValues values = new ContentValues();
2
3 values.put(Clientes.COL_NOMBRE, "ClienteN");
4 values.put(Clientes.COL_TELEFONO, "999111222");
5 values.put(Clientes.COL_EMAIL, "nuevo@email.com");
6
7 ContentResolver cr = getContentResolver();
8
9 cr.insert(ClientesProvider.CONTENT_URI, values);
Por ltimo, y ms sencillo todava, la eliminacin de registros la haremos directamente
utilizando el mtododelete() del content resolver, indicando como segundo parmetro el
criterio de localizacin de los registros que queremos eliminar, que en este caso sern los
que hayamos insertado nuevos con el segundo botn de ejemplo (aquellos con nombre =
ClienteN).
1 ContentResolver cr = getContentResolver();
2
3 cr.delete(ClientesProvider.CONTENT_URI,
4
Clientes.COL_NOMBRE + " = 'ClienteN'", null);
Como muestra grfica, veamos por ejemplo el resultado de la consulta de clientes (primer
botn) en la aplicacin de ejemplo.
288
Con esto, hemos visto lo sencillo que resulta acceder a los datos proporcionados por un
content provider. Pues bien, ste es el mismo mecanismo que podemos utilizar para acceder
a muchos datos de la propia plataforma Android. En la documentacin oficial
del paquete android.provider podemos consultar los datos que tenemos disponibles a travs
de este mecanismo, entre ellos encontramos por ejemplo: el historial de llamadas, la agenda
de contactos y telfonos, las bibliotecas multimedia (audio y video), o el historial y la lista
de favoritos del navegador.
Por ver un ejemplo de acceso a este tipo de datos, vamos a realizar una consulta al historial
de
llamadas
del
dispositivo,
para
lo
que
accederemos
al
content
provider android.provider.CallLog.
En primer lugar vamos a registrar varias llamadas en el emulador de Android, de forma que
los resultados de la consulta al historial de llamadas contenga algunos registros. Haremos
por ejemplo varias llamadas salientes desde el emulador y simularemos varias llamadas
entrantes desde el DDMS. Las primeras son sencillas, simplemente ve al emulador, accede
al telfono,marca y descuelga igual que lo haras en un dispositivo fsico. Y para emular
llamadas entrantes podremos hacerlo una vez ms desde Eclipse, accediendo a la vista del
DDMS. En esta vista, si accedemos a la seccin Emulator Control veremos un apartado
llamado Telephony Actions. Desde ste, podemos introducir un nmero de telfono
origen cualquiera y pulsar el botn Call para conseguir que nuestro emulador reciba una
llamada entrante. Sin aceptar la llamada en elemulador pulsaremos Hang Up para teminar
la llamada simulando as una llamada perdida.
289
290
18
String tipoLlamada = "";
19
String telefono;
20
21
int colTipo = cur.getColumnIndex(Calls.TYPE);
22
int colTelefono = cur.getColumnIndex(Calls.NUMBER);
23
24
txtResultados.setText("");
25
26
do
27
{
28
tipo = cur.getInt(colTipo);
29
telefono = cur.getString(colTelefono);
30
31
if(tipo == Calls.INCOMING_TYPE)
32
tipoLlamada = "ENTRADA";
33
else if(tipo == Calls.OUTGOING_TYPE)
34
tipoLlamada = "SALIDA";
35
else if(tipo == Calls.MISSED_TYPE)
36
tipoLlamada = "PERDIDA";
37
38
txtResultados.append(tipoLlamada + " - " + telefono + "\n");
39
40
} while (cur.moveToNext());
41 }
Lo nico fuera de lo normal que hacemos en el cdigo anterior es la decodificacin del
valor del tipo de llamada recuperado, que la hacemos comparando el resultado con las
constantes Calls.INCOMING_TYPE(entrante), Calls.OUTGOING_TYPE (saliente), Calls.
MISSED_TYPE (perdida) proporcionadas por la propia clase provider.
Un ltimo detalle importante. Para que nuestra aplicacin pueda acceder al historial de
llamadas del dispositivo tendremos que incluir en el fichero AndroidManifest.xml el
permiso READ_CONTACTS yREAD_CALL_LOG utilizando
la
clusula <usespermission> correspondiente.
<uses-permission android:name="android.permission.READ_CONTACTS"></uses1 permission>
2 <uses-permission android:name="android.permission.READ_CALL_LOG"></usespermission>
Si ejecutamos la aplicacin y realizamos la consulta podremos ver un resultado similar al
siguiente:
291
Y con esto terminamos con el tema dedicado a los content providers. Espero que os haya
sido til para aprender a incluir esta funcionalidad a vuestras aplicaciones y a utilizar este
mecanismo para acceder a datos propios del sistema.
Notificaciones en Android
Notificaciones en Android (I): Toast
by Sgoliver on 09/06/2011 in Android, Programacin
Un tema rpido antes de seguir con el Curso de Programacin Android que estamos
realizando. En Android existen varias formas de notificar mensajes al usuario, como por
ejemplo los cuadros de dilogo modales o las notificaciones de la bandeja del sistema (o
barra de estado). Pero en este artculo nos vamos a centrar en primer lugar en la forma ms
sencilla de notificacin: los llamados Toast.
Un toast es un mensaje que se muestra en pantalla durante unos segundos al usuario para
luego volver a desaparecer automticamente sin requerir ningn tipo de actuacin por su
parte, y sin recibir el foco en ningn momento (o dicho de otra forma, sin interferir en las
acciones que est realizando el usuario en ese momento). Aunque son personalizables,
aparecen por defecto en la parte inferior de la pantalla, sobre un rectngulo gris ligeramente
translcido. Por sus propias caractersticas, este tipo de notificaciones son ideales para
mostrar mensajes rpidos y sencillos al usuario, pero por el contrario, al no requerir
confirmacin por su parte, no deberan utilizarse para hacer notificaciones demasiado
importantes.
Su utilizacin es muy sencilla, concentrndose toda la funcionalidad en la clase Toast. Esta
clase dispone de un mtodo esttico makeText() al que deberemos pasar como parmetro el
contexto de la actividad, el texto a mostrar, y la duracin del mensaje, que puede tomar los
valores LENGTH_LONG o LENGTH_SHORT, dependiendo del tiempo que queramos
que la notificacin aparezca en pantalla. Tras obtener una referencia al objeto Toast a travs
de este mtodo, ya slo nos quedara mostrarlo en pantalla mediante el mtodo show().
292
Vamos a construir una aplicacin de ejemplo para demostrar el funcionamiento de este tipo
de notificaciones. Y para empezar vamos a incluir un botn que muestre un toast bsico de
la forma que acabamos de describir:
1
btnDefecto.setOnClickListener(new OnClickListener() {
2
@Override
3
public void onClick(View arg0) {
4
Toast toast1 =
5
Toast.makeText(getApplicationContext(),
6
"Toast por defecto", Toast.LENGTH_SHORT);
7
8
toast1.show();
9
}
10 });
Si ejecutamos esta sencilla aplicacin en el emulador y pulsamos el botn que acabamos de
aadir veremos como en la parte inferior de la pantalla aparece el mensaje Toast por
defecto, que tras varios segundos desaparecer automticamente.
Como hemos comentado, ste es el comportamiento por defecto de las notificaciones toast,
sin embargo tambin podemos personalizarlo un poco cambiando su posicin en la
pantalla. Para esto utilizaremos el mtodo setGravity(), al que podremos indicar en qu
zona deseamos que aparezca la notificacin. La zona deberemos indicarla con alguna de las
constantes definidas en la clase Gravity: CENTER, LEFT,BOTTOM, o con alguna
combinacin de stas.
293
294
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/lytLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal"
android:background="#555555"
android:padding="5dip" >
<ImageView android:id="@+id/imgIcono"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/marcador" />
<TextView android:id="@+id/txtMensaje"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textColor="#FFFFFF"
android:paddingLeft="10dip" />
</LinearLayout>
295
11
12
13
14
15
16
17
txtMsg.setText("Toast Personalizado");
toast3.setDuration(Toast.LENGTH_SHORT);
toast3.setView(layout);
toast3.show();
}
});
296
297
podemos
acceder
mediante
una
constante Context.NOTIFICATION_SERVICE.
llamada
Por
su
a getSystemService() con
parte
al
la
mtodo notify() le
298
pasaremos como parmetro un identificador nico definido por nosotros que identifique
nuestra notificacin y el resultado del builder que hemos construido antes, que obtenemos
llamando a su mtodo build().
1 NotificationManager mNotificationManager =
2
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
3
4 mNotificationManager.notify(NOTIF_ALERTA_ID, mBuilder.build());
Ya estamos en disposicin de probar nuestra aplicacin de ejemplo. Para ello vamos a
ejecutarla en el emulador de Android y pulsamos el botn que hemos implementado con
todo el cdigo anterior. Si todo va bien, debera aparecer en ese mismo momento nuestra
notificacin en la barra de estado, con el icono y texto definidos.
299
mecanismo que podemos utilizar para mostrar o solicitar informacin puntual al usuario. El
mecanismo del que hablamos son los cuadros de dilogo.
En principio, los dilogos de Android los podremos utilizar con distintos fines, en general:
Mostrar un mensaje.
Pedir una confirmacin rpida.
Solicitar al usuario una eleccin (simple o mltiple) entre varias alternativas.
De cualquier forma, veremos tambin cmo personalizar completamente un dilogo para
adaptarlo a cualquier otra necesidad.
El uso actual de los dilogos en Android se basa en fragmets, pero por suerte tenemos toda
la funcionalidad implementada una vez ms en la librera de compatibilidad androidsupport-v4.jar (que debe estar incluida por defecto en tu proyecto si lo has creado con una
versin reciente del pluin de Eclipse) por lo que no tendremos problemas al ejecutar nuestra
aplicacin en versiones antiguas de Android. En este caso nos vamos a basar en la
clase DialogFragment. Para crear un dilogo lo primero que haremos ser crear una nueva
clase
que
herede
de DialogFragment y
sobrescribiremos
uno
de
sus
mtodosonCreateDialog(), que ser el encargado de construir el dilogo con las opciones
que necesitemos.
La forma de construir cada dilogo depender de la informacin y funcionalidad que
necesitemos. A continuacin mostrar algunas de las formas ms habituales.
Dilogo de Alerta
Este tipo de dilogo se limita a mostrar un mensaje sencillo al usuario, y un nico botn de
OK para confirmar su lectura. Lo construiremos mediante la clase AlertDialog, y ms
concretamente su subclaseAlertDialog.Builder, de forma similar a las notificaciones de
barra de estado que ya hemos comentado en el captulo anterior. Su utilizacin es muy
sencilla, bastar con crear un objeto de tipoAlertDialog.Builder y establecer las propiedades
del dilogo mediante sus mtodos correspondientes: ttulo [setTitle()], mensaje
[setMessage()] y el texto y comportamiento del botn [setPositiveButton()]. Veamos un
ejemplo:
1
public class DialogoAlerta extends DialogFragment {
2
@Override
3
public Dialog onCreateDialog(Bundle savedInstanceState) {
4
5
AlertDialog.Builder builder =
6
new AlertDialog.Builder(getActivity());
7
8
builder.setMessage("Esto es un mensaje de alerta.")
300
9
10
11
12
13
14
15
16
17
18
.setTitle("Informacin")
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
return builder.create();
}
}
Dilogo de Confirmacin
Un dilogo de confirmacin es muy similar al anterior, con la diferencia de que lo
utilizaremos para solicitar al usuario que nos confirme una determinada accin, por lo que
las posibles respuestas sern del tipo S/No.
301
Dilogo de Seleccin
302
Cuando las opciones a seleccionar por el usuario no son slo dos, como en los dilogos de
confirmacin, sino que el conjunto es mayor podemos utilizar los dilogos de seleccin
para mostrar una lista de opciones entre las que el usuario pueda elegir.
Para ello tambin utilizaremos la clase AlertDialog, pero esta vez no asignaremos ningn
mensaje ni definiremos las acciones a realizar por cada botn individual, sino que
directamente indicaremos la lista de opciones a mostrar (mediante el mtodo setItems()) y
proporcionaremos la implementacin del eventoonClick() sobre dicha lista (mediante un
listener de tipo DialogInterface.OnClickListener), evento en el que realizaremos las
acciones oportunas segn la opcin elegida. La lista de opciones la definiremos como un
array tradicional. Veamos cmo:
1
public class DialogoSeleccion extends DialogFragment {
2
@Override
3
public Dialog onCreateDialog(Bundle savedInstanceState) {
4
5
final String[] items = {"Espaol", "Ingls", "Francs"};
6
7
AlertDialog.Builder builder =
8
new AlertDialog.Builder(getActivity());
9
10
builder.setTitle("Seleccin")
11
.setItems(items, new DialogInterface.OnClickListener() {
12
public void onClick(DialogInterface dialog, int item) {
13
Log.i("Dialogos", "Opcin elegida: " + items[item]);
14
}
15
});
16
17
return builder.create();
18
}
19 }
En este caso el dilogo tendr un aspecto similar a la interfaz mostrada para los
controles Spinner.
303
Este dilogo permite al usuario elegir entre las opciones disponibles cada vez que se
muestra en pantalla. Pero, y si quisiramos recordar cul es la opcin u opciones
seleccionadas por el usuario para que aparezcan marcadas al visualizar de nuevo el cuadro
de
dilogo?
Para
ello
podemos
utilizar
los
mtodossetSingleChoiceItems() o setMultiChiceItems(), en vez de el setItems() utilizado
anteriormente. La diferencia entre ambos mtodos, tal como se puede suponer por su
nombre, es que el primero de ellos permitir una seleccin simple y el segundo una
seleccin mltiple, es decir, de varias opciones al mismo tiempo, mediante
controles CheckBox.
La forma de utilizarlos es muy similar a la ya comentada. En el caso
de setSingleChoiceItems(), el mtodo tan slo se diferencia de setItems() en que recibe un
segundo parmetro adicional que indica el ndice de la opcin marcada por defecto. Si no
queremos tener ninguna de ellas marcadas inicialmente pasaremos el valor -1.
1 builder.setTitle("Seleccin")
2
.setSingleChoiceItems(items, -1,
3
new DialogInterface.OnClickListener() {
4
public void onClick(DialogInterface dialog, int item) {
5
Log.i("Dialogos", "Opcin elegida: " + items[item]);
6
}
7
});
De esta forma conseguiramos un dilogo como el de la siguiente imagen:
304
que ser un array de booleanos. En caso de no querer ninguna opcin seleccionada por
defecto pasaremos el valor null.
1 builder.setTitle("Seleccin")
2
.setMultiChoiceItems(items, null,
3
new DialogInterface.OnMultiChoiceClickListener() {
4
public void onClick(DialogInterface dialog, int item, boolean isChecked) {
5
Log.i("Dialogos", "Opcin elegida: " + items[item]);
6
}
7 });
Y el dilogo nos quedara de la siguiente forma:
Tanto si utilizamos la opcin de seleccin simple como la de seleccin mltiple, para salir
del dilogo tendremos que pulsar la tecla Atrs de nuestro dispositivo.
Dilogos Personalizados
Por ltimo, vamos a comentar cmo podemos establecer completamente el aspecto de un
cuadro de dilogo. Para esto vamos a actuar como si estuviramos definiendo la interfaz de
una actividad, es decir, definiremos un layout XML con los elementos a mostrar en el
dilogo. En mi caso voy a definir un layout de ejemplo llamado dialog_personal.xml que
colocar como siempre en la carpeta res/layout. Contendr por ejemplo una imagen a la
izquierda y dos lneas de texto a la derecha:
1
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
android:layout_width="match_parent"
3
android:layout_height="match_parent"
4
android:orientation="horizontal"
5
android:padding="3dp" >
6
7
<ImageView
8
android:id="@+id/imageView1"
9
android:layout_width="wrap_content"
10
android:layout_height="wrap_content"
11
android:src="@drawable/ic_launcher" />
12
305
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Por
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="3dp">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialogo_linea_1" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialogo_linea_2" />
</LinearLayout>
</LinearLayout>
su
parte,
en
el
utilizaremos
el
mtodo setView() del builder para asociarle nuestro layout personalizado, que previamente
tendremos que inflar como ya hemos comentado otras veces utilizando el mtodo inflate().
Finalmente podremos incluir botones tal como vimos para los dilogos de alerta o
confirmacin. En este caso de ejemplo incluiremos un botn de Aceptar.
1
public class DialogoPersonalizado extends DialogFragment {
2
@Override
3
public Dialog onCreateDialog(Bundle savedInstanceState) {
4
5
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
6
LayoutInflater inflater = getActivity().getLayoutInflater();
7
8
builder.setView(inflater.inflate(R.layout.dialog_personal, null))
9
.setPositiveButton("Aceptar", new DialogInterface.OnClickListener() {
10
public void onClick(DialogInterface dialog, int id) {
11
dialog.cancel();
12
}
13
});
14
15
return builder.create();
16
}
17 }
306
307
forma gratuita las versionesExpress de ambos productos (ms que suficientes para crear una
aplicacin como la que describiremos en este artculo) desde la web oficial de Microsoft.
Tambin es recomendable instalar SQL Server 2008 Management Studio Express,
descargable tambin de forma gratuita desde esta web. Esta aplicacin no es ms que un
gestor grfico para acceder y manipular nuestras bases de datos SQL Server con total
comodidad.
Vamos comenzar por la base de todo el sistema, y esto es la base de datos a la que acceder
el servicio web y, a travs de ste, tambin la aplicacin Android que crearemos ms
adelante. Para ello abrimos SQL Server Management Studio, nos conectamos a nuestro
servidor SQL Server local, y pulsamos sobre la seccin Databases del rbol de objetos
que aparece a la izquierda. Sobre esta carpeta podemos acceder a la opcin New
Database del men contextual para crear una nueva base de datos.
En el cuadro de dilogo que aparece tan slo indicaremos el nombre de la nueva base de
datos, en mi caso la llamar DBCLIENTES, y dejaremos el resto de opciones con sus
valores por defecto.
308
IdCliente, de tipo int, que ser un cdigo nico identificativo del cliente.
Nombre, de tipo nvarchar(50), que contendr el nombre del cliente.
Telefono, de tipo int, que contendr el telfono del cliente.
Marcaremos adems el campo IdCliente como clave principal de la tabla, y tambin
como campo de identidad autoincremental, de modo que se calcule automticamente cada
vez que insertemos un nuevo cliente.
309
Con esto ya tenemos nuestra tabla finalizada, por lo que slo nos queda guardarla con el
nombre que deseemos, que para este ejemplo ser Clientes.
Hecho, ya tenemos nuestra base de datos SQL Server creada y una tabla preparada para
almacenar los datos asociados a nuestros clientes. El siguiente paso ser crear el servicio
web que manipular los datos de esta tabla.
Para crear el servicio abriremos Visual Studio 2010 y crearemos un nuevo proyecto web en
C# utilizando la plantilla ASP.NET Empty Web Application. En un alarde de originalidad
lo llamaremos ServicioWebSoap.
Una vez creado el proyecto, aadiremos a ste un nuevo servicio web mediante el men
Project / Add new item. Lo llamaremos ServicioClientes.asmx.
310
Una vez aadido aparecer en pantalla el cdigo fuente por defecto del nuevo servicio web,
que contiene un nico mtodo de ejemplo llamado HelloWorld(). Este mtodo podemos
eliminarlo ya que no nos servir de nada, y adems modificaremos el
atributo WebService de la clase para indicar que el namespace ser http://sgoliver.net/
(en vuestro caso podis indicar otro valor). Con esto, nos quedara un cdigo base como
ste:
1
namespace ServicioWebSoap
2
{
3
/// <summary>
4
/// Summary description for ServicioClientes
5
/// </summary>
6
[WebService(Namespace = "http://sgoliver.net/")]
7
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
8
[System.ComponentModel.ToolboxItem(false)]
9
10
public class ServicioClientes : System.Web.Services.WebService
11
{
12
//Mtodos del servicio web
13
//...
14
}
15 }
Pues bien, dentro de esta clase ServicioClientes es donde aadiremos todos los mtodos
pblicos que queramos tener accesibles a travs de nuestro servicio web, siempre
precedidos por el atributo[WebMethod] como veremos en breve. Para nuestro ejemplo
vamos a crear tres mtodos, el primero para obtener el listado completo de clientes
almacenados en la base de datos, y los otros dos para insertar nuevos clientes (ms adelante
explicar por qu dos, aunque adelanto que es tan slo por motivos didcticos).
Antes de crear estos mtodos, vamos a crear una nueva clase sencilla que nos sirva para
encapsular los datos de un cliente. La aadiremos mediante la opcin Project / Add
class de Visual Studio y la llamaremos Cliente.cs. Esta clase contendr nicamente
los 3 campos que ya comentamos al crear la base de datos y dos constructores, uno de ellos
por defecto que tan solo inicializar los campos y otro con parmetros para crear clientes a
311
partir de sus datos identificativos. El cdigo de la clase es muy sencillo, y tan solo cabe
mencionar que definiremos sus tres atributos como propiedades automticas de C#
utilizando para ello la notacin abreviada {get; set;}
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Web;
5
6
namespace ServicioWebSoap
7
{
8
public class Cliente
9
{
10
public int Id {get; set;}
11
public string Nombre {get; set;}
12
public int Telefono {get; set;}
13
14
public Cliente()
15
{
16
this.Id = 0;
17
this.Nombre = "";
18
this.Telefono = 0;
19
}
20
21
public Cliente(int id, string nombre, int telefono)
22
{
23
this.Id = id;
24
this.Nombre = nombre;
25
this.Telefono = telefono;
26
}
27
}
28 }
Vamos ahora a escribir el primero de los mtodos que haremos accesible a travs de
nuestro servicio web. Lo llamaremos NuevoCliente(), recibir como parmetros de entrada
un nombre y un telfono, y se encargar de insertar un nuevo registro en nuestra tabla de
clientes con dichos datos. Recordemos que el ID del cliente no ser necesario insertarlo de
forma explcita ya que lo hemos definido en la base de datos como campo autoincremental.
Para el trabajo con la base de datos vamos a utilizar la API clsica deADO.NET, aunque
podramos utilizar cualquier otro mcanismo de acceso a datos, como por ejemploEntity
Framework, NHibernate, etc.
De esta forma, el primer paso ser crear una conexin a SQL Server mediante la
clase SQLConnection, pasando como parmetro la cadena de conexin correspondiente (en
vuestro caso tendris que modificarla para adaptarla a vuestro entorno). Tras esto abriremos
312
la conexin mediante una llamada al mtodo Open(), definiremos el comando SQL que
queremos ejecutar creando un objeto SQLCommand. Ejecutaremos el comando llamando al
mtodo ExecuteNonQuery() recogiendo el resultado en una variable, y finalmente
cerraremos la conexin llamando a Close(). Por ltimo devolveremos el resultado del
comando SQL como valor de retorno del mtodo web.
Como podis ver en el cdigo siguiente, los valores a insertar en la base de datos los hemos
especificado en la consulta SQL como parmetros variable (precedidos por el carcter @).
Los valores de estos parmetros los definimos y aadimos al comando SQL mediante el
mtodo Add() de su propiedadParameters. Esta opcin es ms recomendable que la opcin
clsica de concatenar directamente la cadena de texto de la sentencia SQL con los
parmetros variables, ya que entre otras cosas servir para evitar [en gran medida] posibles
ataques de inyeccin SQL. El resultado devuelto por este mtodo ser el nmero de
registros afectados por la sentencia SQL ejecutada, por lo que para verificar si se ha
ejecutado correctamente bastar con comprobar que el resultado es igual a 1.
[WebMethod]
1
public int NuevoClienteSimple(string nombre, int telefono)
2
{
3
SqlConnection con =
4
new SqlConnection(
5
@"Data
Source=SGOLIVERPC\SQLEXPRESS;Initial
Catalog=DBCLIENTES;Integrate
6
Security=True");
7
8
con.Open();
9
10
string sql = "INSERT INTO Clientes (Nombre, Telefono) VALUES (@nombre, @telefono)";
11
12
SqlCommand cmd = new SqlCommand(sql, con);
13
14
cmd.Parameters.Add("@nombre", System.Data.SqlDbType.NVarChar).Value = nombre;
15
cmd.Parameters.Add("@telefono", System.Data.SqlDbType.Int).Value = telefono;
16
17
int res = cmd.ExecuteNonQuery();
18
19
con.Close();
20
21
return res;
22
}
En el cdigo anterior, podis ver que hemos precedido el mtodo con el
atributo [WebMethod]. Con este atributo estamos indicando que el mtodo ser accesible a
travs de nuestro servicio web y podr ser llamado desde cualquier aplicacin que se
conecte con ste.
313
La siguiente operacin que vamos a aadir a nuestro servicio web ser la que nos permita
obtener el listado completo de clientes registrados en la base de datos. Llamaremos al
mtodo ListadoClientes()y devolver un array de objetos de tipo Cliente. El cdigo del
mtodo ser muy similar al ya comentado para la operacin de insercin, con la nica
diferencia de que en esta ocasin la sentencia SQL ser obviamente un SELECT y que
utilizaremos un objeto SqlDataReader para leer los resultados devueltos por la consulta.
Los registros ledos los iremos aadiendo a una lista de tipo List<Clientes> y una vez
314
objeto complejo, en concreto un array de objetos de tipo Cliente. Sin embargo, ninguno de
ellos recibe como parmetro un tipo complejo, tan slo valores simples (enteros y strings).
Esto no tiene mucha relevancia en el cdigo de nuestro servicio web, pero s tiene ciertas
peculiaridades a la hora de realizar la llamada al servicio desde la aplicacin Android. Por
lo que para poder explicar esto ms adelante aadiremos un nuevo mtodo de insercin de
clientes que, en vez de recibir los parmetros de nombre y telfono por separado, recibir
como dato de entrada un objetoCliente.
315
Si pulsamos sobre cualquiera de ellas pasaremos a una nueva pgina que nos permitir dar
valores a sus parmetros y ejecutar el mtodo correspondiente para visualizar sus
resultados. Si pulsamos por ejemplo en la operacin NuevoCliente(string, int) llegaremos a
esta pgina:
Aqu podemos dar valores a los dos parmetros y ejecutar el mtodo (botn Invoke), lo
que nos devolver la respuesta codificada en un XML segn el estandar SOAP.
Como podis comprobar, en principio el XML devuelto no es fcil de interpretar, pero esto
es algo que no debe preocuparnos demasiado ya que en principio ser transparente para
nosotros, las libreras que utilizaremos ms adelante en Android para la llamada a servicios
SOAP se encargarn de parsearconvenientemente estas respuestas y de darnos tan slo
aquella parte que necesitamos.
En el siguiente artculo nos ocuparemos de la construccin de una aplicacin Android que
sea capaz de conectarse a este servicio web y de llamar a los mtodos que hemos definido
para insertar y recuperar clientes de nuestra base de datos. Veremos adems cmo podemos
ejecutar y probar en local todo el sistema de forma que podamos comprobar que todo
funciona como esperamos.
Acceso a Servicios Web SOAP en Android (2/2)
by Sgoliver on 27/02/2012 in Android, Programacin
En el artculo anterior del curso vimos cmo construir un servicio web SOAP haciendo uso
de ASP.NET y una base de datos externa SQL Server. En este segundo artculo veremos
cmo podemos acceder a este servicio web desde una aplicacin Android y probaremos
todo el sistema en local para verificar su correcto funcionamiento.
316
En primer lugar hay que empezar diciendo que Android no incluye de serie ningn tipo
de soporte para el acceso a servicios web de tipo SOAP. Es por esto por lo que vamos a
utilizar una librera externa para hacernos ms fcil esta tarea. Entre la oferta actual, la
opcin ms popular y ms utilizada es la libreraksoap2-android. Esta librera es un fork,
especialmente adaptado para Android, de la antigua librerakSOAP2. Este framework nos
permitir de forma relativamente fcil y cmoda utilizar servicios web que utilicen el
estndar SOAP. La ltima versin de esta librera en el momento de escribir este artculo es
la 2.6.0, que puede descargarse desde este enlace.
Agregar esta librera a nuestro proyecto Android es muy sencillo. Si tenemos una versin
reciente del plugin de Android para Eclipse, una vez tenemos creado el proyecto en
Android bastar con copiar el archivo .jar en la carpeta libs de nuestro proyecto. Si tu
versin del plugin es ms antigua es posible que tengas que adems aadir la librera
al path del proyecto. Para ello accederemos al men Project / Properties y en la ventana
de propiedades accederemos a la seccin Java Build Path. En esta seccin accederemos a
la solapa Libraries y pulsaremos el botn Add External JARs. Aqu seleccionamos el
fichero jar de la librera ksoap2-android (en este caso ksoap2-android-assembly-3.6.0-jarwith-dependencies.jar) y listo, ya tenemos nuestro proyecto preparado para hacer uso de la
funcionalidad aportada por la librera.
Como aplicacin de ejemplo, vamos a crear una aplicacin sencilla que permita aadir un
nuevo usuario a la base de datos. Para ello aadiremos a la vista principal dos cuadros de
texto para introducir el nombre y telfono del nuevo cliente (en mi caso se
llamarn txtNombre y txtTelefono respectivamente) y un botn (en mi caso btnEnviar) que
realice la llamada al mtodo NuevoCliente del servicio web pasndole como parmetros los
datos introducidos en los cuadros de texto anteriores.
No voy a mostrar todo el cdigo necesario para crear esta vista y obtener las referencias a
cada control porque no tiene ninguna particularidad sobre lo ya visto en multitud de
ocasiones en artculos anteriores del curso (en cualquier caso al final del artculo podis
descargar todo el cdigo fuente para su consulta). Lo que nos interesa en este caso es la
implementacin del evento onClick del botn btnEnviar, que ser el encargado de
comunicarse con el servicio web y procesar el resultado.
Lo primero que vamos a hacer en este evento es definir, por comodidad, cuatro constantes
que nos servirn en varias ocasiones durante el cdigo:
317
Aunque los valores se podran ms o menos intuir, para conocer exactamente los valores
que debemos asignar a estas constantes vamos a ejecutar una vez ms el proyecto de Visual
Studio que construimos en el artculo anterior y vamos a acceder a la pgina de prueba del
mtodo NuevoCliente. Veremos algo parecido a lo siguiente:
En la imagen anterior se muestran resaltados en rojo los valores de las cuatro constantes a
definir, que en nuestro caso concreto quedaran de la siguiente forma:
1
2
3
4
318
Volvamos a la pgina de prueba del mtodo web NuevoCliente. Justo debajo de la seccin
donde se solicitan los parmetros a pasar al mtodo se incluye tambin un XML de muestra
de cmo tendra que ser nuestra peticin al servidor si tuviramos que construirla a mano.
Echmosle un vistazo:
Una vez ms he marcado varias zonas sobre la imagen, correspondientes a las tres partes
principales de una peticin de tipo SOAP. Empezando por la parte interna del XML, en
primer lugar encontramos los datos de la peticin en s (Request) que contiene el nombre
del mtodo al que queremos llamar, y los nombres y valores de los parmetros en entrada.
Rodeando a esta informacin se aaden otra serie de etiquetas y datos a modo
de contenedor estndar que suele recibir el nombre de Envelope. La informacin indicada
en este contenedor no es especfica de nuestra llamada al servicio, pero s contiene
informacin sobre formatos y esquemas de validacin del estndar SOAP. Por ltimo,
durante el envo de esta peticin SOAP al servidor mediante el protocolo HTTP se aaden
determinados encabezados como los que veis en la imagen. Todo esto junto har que el
servidor sea capaz de interpretar correctamente nuestra peticin SOAP, se llame al mtodo
web correcto, y se devuelva el resultado en un formato similar al anterior que ya veremos
ms adelante. Aclarada un poco la estructura y funcionamiento general de una peticin
SOAP veamos lo sencillo que resulta realizarla desde nuestra aplicacin Android.
En primer lugar crearemos la peticin (request) a nuestro mtodo NuevoCliente. Para ello
crearemos un nuevo objeto SoapObject pasndole el namespace y el nombre del mtodo
web. A esta peticin tendremos que asociar los parmetros de entrada mediante el
mtodo addProperty() al que pasaremos los nombres y valores de los parmetros (que en
nuestro caso se obtendrn de los cuadros de texto de la vista principal).
1 SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME);
2
3 request.addProperty("nombre", txtNombre.getText().toString());
4 request.addProperty("telefono", txtTelefono.getText().toString());
El segundo paso ser crear el contenedor SOAP (envelope) y asociarle nuestra peticin.
Para ello crearemos un nuevo objeto SoapSerializationEnvelope indicando la versin de
SOAP que vamos a usar (versin 1.1 en nuestro caso, como puede verse en la imagen
anterior). Indicaremos adems que se trata de un servicio web .NET activando su
319
propiedad dotNet. Por ltimo, asociaremos la peticin antes creada a nuestro contenedor
llamando al mtodo setOutputSoapObject().
1 SoapSerializationEnvelope envelope =
2
new SoapSerializationEnvelope(SoapEnvelope.VER11);
3
4 envelope.dotNet = true;
5
6 envelope.setOutputSoapObject(request);
Como tercer paso crearemos el objeto que se encargar de realizar la comunicacin HTTP
con el servidor, de tipo HttpTransportSE, al que pasaremos la URL de conexin a nuestro
servicio web. Por ltimo, completaremos el proceso realizando la llamada al servicio web
mediante el mtodo call().
1
HttpTransportSE transporte = new HttpTransportSE(URL);
2
3
try
4
{
5
transporte.call(SOAP_ACTION, envelope);
6
7
//Se procesa el resultado devuelto
8
//...
9
}
10 catch (Exception e)
11 {
12
txtResultado.setText("Error!");
13 }
Tras la llamada al servicio ya estamos en disposicin de obtener el resultado devuelto por el
mtodo web llamado. Esto lo conseguimos mediante el mtodo getResponse().
Dependiendo del tipo de resultado que esperemos recibir deberemos convertir esta
respuesta a un tipo u otro. En este caso, como el resultado que esperamos es un valor
simple (un nmero entero) convertiremos la respuesta a un objetoSoapPrimitive, que
directamente podremos convertir a una cadena de caracteres llamado atoString(). Ms
adelante veremos cmo tratar valores de retorno ms complejos.
1 SoapPrimitive resultado_xml =(SoapPrimitive)envelope.getResponse();
2 String res = resultado_xml.toString();
3
4 if(res.equals("1"))
5
txtResultado.setText("Insertado OK");
Y listo, con esto ya tenemos preparada la llamada a nuestro servicio web y el tratamiento de
la respuesta recibida.
320
En la imagen vemos cmo hemos insertado un nuevo cliente llamada cliente7 con nmero
de telfono 7777. Si consultamos ahora nuestra base de datos Sql Server podremos
comprobar si el registro efectivamente se ha insertado correctamente.
Algo importante que quiero remarcar llegados a este punto. El cdigo anterior debe
funcionar correctamente sobre un dispositivo o emulador con versin de Android anterior a
la 3.0. Sin embargo, si intentamos ejecutar la aplicacin sobre una versin posterior
321
322
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
Como
podemos
ver,
prcticamente
todo
el
cdigo
se
ha
trasladado
al
323
2
3
4
5
6
7
8
@Override
public void onClick(View v) {
TareaWSConsulta tarea = new TareaWSConsulta();
tarea.execute();
}
});
Con esto, ya sabemos realizar una llamada a un servicio web SOAP que devuelve un valor
de retorno sencillo, en este caso un simple nmero entero. Lo siguiente que vamos a ver
ser como implementar la llamada a un mtodo del servicio web que nos devuelva un valor
algo ms complejo. Y esto lo vamos a ver con la llamada al mtodo
web ListadoClientes() que recordemos devolva un array de objetos de tipoCliente.
En este caso, la llamada al mtodo web se realizar de forma totalmente anloga a la ya
comentada. Donde llegarn las diferencias ser a la hora de tratar el resultado devuelto por
el servicio, comenzando por el resultado del mtodo getResponse() de ksoap. En esta
ocasin, dado que el resultado esperado no es ya un valor simple sino un objeto ms
complejo, convertiremos el resultado de getResponse() al tipoSoapObject, en vez
de SoapPrimitive como hicimos anteriormente. Nuestro objetivo ser generar un array de
objetos Cliente (lo llamaremos listaClientes) a partir del resultado devuelto por la llamada
al servicio.
Como sabemos que el resultado devuelto por el servicio es tambin un array, lo primero
que haremos ser crear un array local con la misma longitud que el devuelto, lo que
conseguiremos mediante el mtodogetPropertyCount(). Tras esto, iteraremos por los
distintos elementos del array devuelto mediante el mtodo getProperty(ind), donde ind ser
el ndice de cada ocurrencia. Cada uno de estos elementos ser a su vez otro objeto de
tipo SoapObject, que representar a un Cliente. Adicionalmente, para cada elemento
accederemos a sus propiedades (Id, Nombre, y Telefono) una vez ms mediante llamadas
agetProperty(), con el ndice de cada atributo, que seguir el mismo orden en que se
definieron. As,getProperty(0) recuperar el Id del cliente, getProperty(1) el nombre,
y getProperty(2) el telfono. De esta forma podremos crear nuestros objetos Cliente locales
a partir de estos datos. Al final de cada iteracin aadimos el nuevo cliente recuperado a
nuestro array. Veamos como quedara todo esto en el cdigo, donde seguro que se entiende
mejor:
1
SoapObject resSoap =(SoapObject)envelope.getResponse();
2
3
Cliente[] listaClientes = new Cliente[resSoap.getPropertyCount()];
4
5
for (int i = 0; i < listaClientes.length; i++)
6
{
324
7
8
9
10
11
12
13
14
15
SoapObject ic = (SoapObject)resSoap.getProperty(i);
Cliente cli = new Cliente();
cli.id = Integer.parseInt(ic.getProperty(0).toString());
cli.nombre = ic.getProperty(1).toString();
cli.telefono = Integer.parseInt(ic.getProperty(2).toString());
listaClientes[i] = cli;
}
En nuestra aplicacin de ejemplo aadimos un nuevo botn y un control tipo lista (lo
llamo lstClientes), de forma que al pulsar dicho botn rellenemos la lista con los nombres
de todos los clientes recuperados. La forma de rellenar una lista con un array de elementos
ya la vimos en los artculos dedicados a los controles de seleccin, por lo que no nos
pararemos a comentarlo. El cdigo sera el siguiente (Nota: s que todo esto se podra
realizar de forma ms eficiente sin necesidad de crear distintos arrays para los clientes y
para el adaptador de la lista, pero lo dejo as para no complicar el tutorial con temas ya
discutidos en otros artculos):
1
//Rellenamos la lista con los nombres de los clientes
2
final String[] datos = new String[listaClientes.length];
3
4
for(int i=0; i<listaClientes.length; i++)
5
datos[i] = listaClientes[i].nombre;
6
7
ArrayAdapter<String> adaptador =
8
new ArrayAdapter<String>(ServicioWebSoap.this,
9
android.R.layout.simple_list_item_1, datos);
10
11 lstClientes.setAdapter(adaptador);
Por ltimo, vamos a ver cmo llamar a un mtodo web que recibe como parmetro algn
objeto complejo. Para ilustrarlo haremos una llamada al segundo mtodo de insercin de
clientes que implementamos en el servicio, NuevoClienteObjeto(). Recordemos que este
mtodo reciba como parmetro de entrada un objeto de tipo Cliente.
Para poder hacer esto, lo primero que tendremos que hacer ser modificar un poco nuestra
claseCliente, de forma que ksoap sepa cmo serializar nuestros objetos Cliente a la hora de
generar las peticiones SOAP correspondientes. Y para esto, lo que haremos ser
325
326
15
16
17
18
19
info.name = "Telefono";
break;
default:break;
}
}
Por ltimo, el mtodo setProperty() ser el encargado de asignar el valor de cada atributo
segn su ndice y el valor recibido como parmetro.
1
@Override
2
public void setProperty(int ind, Object val) {
3
switch(ind)
4
{
5
case 0:
6
id = Integer.parseInt(val.toString());
7
break;
8
case 1:
9
nombre = val.toString();
10
break;
11
case 2:
12
telefono = Integer.parseInt(val.toString());
13
break;
14
default:
15
break;
16
}
17 }
Mediante estos mtodos, aunque de forma transparente para el programados, ksoap ser
capaz de transformar nuestros objetos Cliente al formato XML correcto de forma que pueda
pasarlos como parmetro en las peticiones SOAP a nuestro servicio.
Por su parte, la llamada al servicio tambin difiere un poco de lo ya comentado a la hora de
asociar los parmetros de entrada del mtodo web. En este caso, construiremos en primer
lugar el objeto Clienteque queremos insertar en la base de datos a partir de los datos
introducidos en la pantalla de nuestra aplicacin de ejemplo. Tras esto crearemos un nuevo
objeto PropertyInfo, al que asociaremos el nombre, valor y tipo de nuestro cliente mediante
sus mtodos setName(), setValue() y setClass()respectivamente. Por ltimo, asociaremos
este cliente como parmetro de entrada al servicio llamando al metodo addProperty() igual
que hemos hecho en las anteriores ocasiones, con la diferencia de que esta vez lo
llamaremos pasndole el objeto PropertyInfo que acabamos de crear. Adems de esto,
tendremos tambin que llamar finalmente al mtodo addMapping() para asociar de alguna
forma nuestro espacio de nombres y nombre de clase Cliente con la clase real java.
Veamos el cdigo para entenderlo mejor:
1
Cliente cli = new Cliente();
2
cli.nombre = txtNombre.getText().toString();
327
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cli.telefono = Integer.parseInt(txtTelefono.getText().toString());
PropertyInfo pi = new PropertyInfo();
pi.setName("cliente");
pi.setValue(cli);
pi.setType(cli.getClass());
request.addProperty(pi);
SoapSerializationEnvelope envelope =
new SoapSerializationEnvelope(SoapEnvelope.VER11);
envelope.dotNet = true;
envelope.setOutputSoapObject(request);
envelope.addMapping(NAMESPACE, "Cliente", cli.getClass());
328
Espero que estos dos ltimos artculos sobre servicios web SOAP y Android os sirvan para
tener un ejemplo completo, tanto de la parte servidor como de la parte cliente, que os sirva
de base para crear nuevos sistemas adaptados a vuestras necesidades.
Acceso a Servicios Web REST en Android (1/2)
by Sgoliver on 04/03/2012 in Android, Programacin
En los dos artculos anteriores (ste y ste) del Curso de Programacin Android nos hemos
ocupado de describir la forma de construir un sistema formado por un servicio web SOAP
que accede a una base de datos externa y una aplicacin Android que, a travs de este
servicio, es capaz de manipular dichos datos.
En este nuevo artculo vamos a crear un sistema similar, pero esta vez haciendo uso de la
otra alternativa por excelencia a la hora de crear servicios web, y no es otra de utilizar
servicios web tipo REST. Las famosas APIs que publican muchos de los sitios web
actualmente no son ms que servicios web de este tipo, aunque en la mayora de los casos
con medidas de seguridad adicionales tales como autenticacin OAuth o similares.
REST tambin se asienta sobre el protocolo HTTP como mecanismo de transporte entre
cliente y servidor, ya veremos despus en qu medida. Y en cuanto al formato de los datos
transmitidos, a diferencia de SOAP, no se impone ninguno en concreto, aunque lo ms
habitual actualmente es intercambiar la informacin en formato XML o JSON. Ya que en el
caso de SOAP utilizamos XML, en este nuevo artculo utilizaremos JSON para construir
nuestro ejemplo.
Tambin vamos a utilizar un framework distinto para construir el servicio, aunque
seguiremos hacindolo en Visual Studio y en lenguaje C#. En este caso, en vez de utilizar
ASP.NET a secas, vamos a utilizar el framework especfico ASP.NET MVC 3, cuyo
sistema de direccionamiento se ajusta mejor a los principios de REST, donde
cada recurso [en nuestro caso cada cliente] debera ser accesible mediante su propia URL
nica. Podis descargar MVC3 desde su pgina oficial de Microsoft.
En este primer artculo sobre servicios REST vamos a describir la construccin del servicio
web en s, y dedicaremos un segundo artculo a explicar cmo podemos acceder a este
servicio desde una aplicacin Android.
Empezamos. Lo primero que vamos a hacer ser crear un nuevo proyecto en Visual Studio
utilizando esta vez la plantilla llamada ASP.NET MVC 3 Web Application, lo llamaremos
ServicioWebRest.
329
En la ventana de opciones del proyecto dejaremos todos los datos que aparecen por defecto
y seleccionaremos como plantilla Empty para crear una aplicacin vaca.
Esto debera crearnos el nuevo proyecto con la estructura de carpetas necesaria, que como
veris es bastante elaborada. En nuestro caso vamos a crear el servicio web de forma
aislada del resto de la aplicacin web, y para ello lo primero que vamos a hacer es aadir
una nueva Area al proyecto, a la que llamaremos por ejemplo Api, lo que nos crear una
estructura de carpetas similar a la de la aplicacin principal pero dentro de una carpeta
independiente. Esto nos permite aislar todo el cdigo y recursos de nuestro servicio web del
resto de la aplicacin web (que en nuestro caso no existir porque no es el objetivo de este
artculo, pero que podramos crear sin problemas si lo necesitramos).
330
Una vez que ya tenemos preparada toda la estructura de nuestro proyecto empecemos a
aadir los elementos necesarios. Lo primero que vamos a crear ser una nueva
clase Cliente, igual que hicimos en el ejemplo anterior con SOAP. La colocaremos en la
carpeta Api/Models y el cdigo es el mismo que ya vimos:
1 namespace ServicioWebRest.Areas.Api.Models
2 {
3
public class Cliente
4
{
5
public int Id { get; set; }
6
public string Nombre { get; set; }
7
public int Telefono { get; set; }
8
}
9 }
El siguiente elemento a aadir ser una nueva clase que contenga todas las operaciones que
queramos realizar sobre nuestra base de datos de clientes. Llamaremos a la
clase ClienteManager. En este caso s vamos a aadir las cuatro operaciones bsicas sobre
clientes, y una adicional para obtener el listado completo, de forma que ms tarde podamos
mostrar la implementacin en Android de todos los posibles tipos de llamada al servicio.
Los mtodos que aadiremos sern los siguientes:
Cliente ObtenerCliente(int id)
List<Clientes> ObtenerClientes()
bool InsertarCliente(Cliente c)
bool ActualizarCliente(Cliente c)
bool EliminarCliente(int id)
Los dos primeros mtodos nos servirn para recuperar clientes de la base de datos, tanto por
su ID para obtener un cliente concreto, como el listado completo que devolver una lista de
clientes. Los otros tres mtodos permitirn insertar, actualizar y eliminar clientes a partir de
331
su ID y los datos de entrada (si aplica). El cdigo de todos estos mtodos es anlogo a los
ya implementados en el caso de SOAP, por lo que no nos vamos a parar en volverlos a
comentar, tan slo decir que utilizan la api clsica de ADO.NET para el acceso a SQL
Server. En cualquier caso, al final del artculo tenis como siempre el cdigo fuente
completo para poder consultar lo que necesitis. A modo de ejemplo veamos la
implementacin de los mtodosObtenerClientes() e InsertarCliente().
1
public bool InsertarCliente(Cliente cli)
2
{
3
SqlConnection con = new SqlConnection(cadenaConexion);
4
5
con.Open();
6
7
string sql = "INSERT INTO Clientes (Nombre, Telefono) VALUES (@nombre,
8
@telefono)";
9
10
SqlCommand cmd = new SqlCommand(sql,con);
11
12
cmd.Parameters.Add("@nombre",
System.Data.SqlDbType.NVarChar).Value
=
13 cli.Nombre;
14
cmd.Parameters.Add("@telefono", System.Data.SqlDbType.Int).Value = cli.Telefono;
15
16
int res = cmd.ExecuteNonQuery();
17
18
con.Close();
19
20
return (res == 1);
21 }
22
23 public List<Cliente> ObtenerClientes()
24 {
25
List<Cliente> lista = new List<Cliente>();
26
27
SqlConnection con = new SqlConnection(cadenaConexion);
28
29
con.Open();
30
31
string sql = "SELECT IdCliente, Nombre, Telefono FROM Clientes";
32
33
SqlCommand cmd = new SqlCommand(sql,con);
34
35
SqlDataReader reader =
36
cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
37
38
while (reader.Read())
39
{
332
40
41
42
43
44
45
46
47
48
49
50
51
}
Hasta ahora, todo el cdigo que hemos escrito es bastante genrico y nada tiene que ver con
que nuestro proyecto sea de tipo MVC. Sin embargo, los dos siguientes elementos s que
estn directamente relacionados con el tipo de proyecto que tenemos entre manos.
Lo siguiente que vamos a aadir ser un controlador a nuestro servicio web. Este
controlador (claseClientesController) ser el encargado de contener las
diferentes acciones que se podrn llamar segn la URL y datos HTTP que recibamos como
peticin de entrada al servicio. Para nuestro ejemplo, aadiremos tan slo dos acciones, una
primera dirigida a gestionar todas las peticiones que afecten a un nico cliente (insertar,
actualizar, eliminar y obtener por ID), y otra que trate la peticin del listado completo de
clientes. Las llamaremos Clientes() y Cliente() respectivamente. Estas acciones harn uso
de una instancia de la clase ClienteManager creada anteriormente para realizar las acciones
necesarias contra la base de datos. Cada accin ser tambin responsable de formatear sus
resultados al formato de comunicacin que hayamos elegido, en nuestro caso JSON.
La accin Clientes es muy sencilla, se limitar a llamar al mtodo ObtenerClientes() y
formatear los resultados como JSON. Para hacer esto ltimo basta con crear directamente
un objeto JsonResultllamado al mtodo Json() pasndole como parmetro de entrada el
objeto a formatear. Todo esto se reduce a una sola linea de cdigo:
1 [HttpGet]
2 public JsonResult Clientes()
3 {
4
return Json(this.clientesManager.ObtenerClientes(),
5
JsonRequestBehavior.AllowGet);
6 }
Habris notado tambin que hemos precedido el mtodo con el atributo [HttpGet]. Para
intentar explicar esto me hace falta seguir hablando de los principios de diseo de REST.
333
Este tipo de servicios utiliza los propios tipos de peticin definidos por el protocolo HTTP
para diferenciar entre las operaciones a realizar por el servicio web. As, el propio tipo de
peticin HTTP realizada (GET, POST, PUT o DELETE), junto con la direccin URL
especificada en la llamada, nos determinar la operacin a ejecutar por el servicio web. En
el caso ya visto, el atributo [HttpGet] nos indica que dicho mtodo se podr ejecutar al
recibirse una peticin de tipo GET.
Entenderemos todo esto mejor ahora cuando veamos el cdigo de la accin Cliente(). En
esta accin, dependiente del tipo de peticin HTTP recibida, tendremos que llamar a un
mtodo u otro del servicio web. As, usaremos POST para las inserciones de
clientes, PUT para las actualizaciones, GET para la consulta por ID y DELETE para las
eliminaciones. En este caso no precedemos el mtodo por ningn atributo, ya que la misma
accin se encargar de tratar diferentes tipos de peticin.
1
public JsonResult Cliente(int? id, Cliente item)
2
{
3
switch (Request.HttpMethod)
4
{
5
case "POST":
6
return Json(clientesManager.InsertarCliente(item));
7
case "PUT":
8
return Json(clientesManager.ActualizarCliente(item));
9
case "GET":
10
return Json(clientesManager.ObtenerCliente(id.GetValueOrDefault()),
11
JsonRequestBehavior.AllowGet);
12
case "DELETE":
13
return Json(clientesManager.EliminarCliente(id.GetValueOrDefault()));
14
}
15
16
return Json(new { Error = true, Message = "Operacin HTTP desconocida" });
17 }
Algunos de vosotros seguro que os estis preguntando cmo distinguir el servicio cundo
llamar a la accin Clientes() para obtener el listado completo, o a la accin Cliente() para
obtener un nico cliente por su ID, ya que para ambas operaciones hemos indicado que se
recibir el tipo de peticin http GET.
Pues bien, aqu es donde nos va a ayudar el ltimo elemento a aadir al servicio web.
Realmente no lo aadiremos, sino que lo modificaremos, ya que es un fichero que ya ha
creado Visual Studio por nosotros. Se trata de la clase ApiAreaRegistration. La funcin de
esta clase ser la de dirigir las peticiones recibidas hacia una accin u otra del controlador
segn la URL utilizada al realizarse la llamada al servicio web.
334
En nuestro caso de ejemplo, vamos a reconocer dos tipos de URL. Una de ellas para
acceder a la lista completa de cliente, y otra para realizar cualquier accin sobre un cliente
en concreto:
335
GET /Api/Clientes
Recuperar el listado completo de clientes y lo devolver en formato JSON.
GET /Api/Clientes/Cliente/3
Recuperar el cliente con el ID indicado en la URL y lo devolver en formato JSON.
POST /Api/Clientes/Cliente { Nombre:nombre, Telefono:1234 }
Insertar un nuevo cliente con los datos aportados en la peticin en formato JSON.
PUT /Api/Clientes/Cliente/3 { Id:3, Nombre:nombre, Telefono:1234 }
Actualizar el cliente con el ID indicado en la URL con los datos aportados en la peticin
en formato JSON.
DELETE /Api/Clientes/Cliente/3
Eliminar el cliente con el ID indicado en la URL.
Llegados aqu, tan slo tenemos que ejecutar nuestro proyecto y esperar a que se abra el
navegador web. En principio no se mostrar un error por no encontrar la pgina principal de
la aplicacin, ya que no hemos creado ninguna, pero nos asegurar que el servidor de
prueba est funcionando, por lo que nuestro servicio debera responder a peticiones.
As, si escribimos en la barra de direcciones por ejemplo la siguiente direccin (el puerto
puede variar):
http://localhost:1234/Api/Clientes/Cliente/4
deberamos recibir un fichero en formato JSON que contuviera los datos del cliente con ID
= 4 de nuestra base de datos. Sera un fichero con contenido similar al siguiente:
{"Id":4,"Nombre":"cliente4","Telefono":4444}
En el siguiente artculo veremos cmo construir una aplicacin Android capaz de acceder a
este servicio y procesar los resultados recibidos.
336
segunda parte vamos a describir cmo podemos construir una aplicacin Android que
acceda a este servicio web REST.
Y tal como hicimos en el caso de SOAP, vamos a crear una aplicacin de ejemplo que
llame a las distintas funciones de nuestro servicio web. En este caso la aplicacin se
compondr de 5 botones, uno por cada una de las acciones que hemos implementado en el
servicio web (insertar, actualizar, eliminar, recuperar un cliente, y listar todos los clientes).
A diferencia del caso de SOAP, en esta ocasin no vamos a utilizar ninguna librera externa
para acceder al servicio web, ya que Android incluye todo lo necesario para realizar la
conexin y llamada a los mtodos del servicio, y tratamiento de resultados en
formato JSON.
Como ya hemos comentado, al trabajar con servicios web de tipo REST, las llamadas al
servicio no se harn a travs de una nica URL, sino que se determinar la accin a realizar
segn la URL accedida y la accin HTTP utilizada para realizar la peticin
(GET, POST, PUT o DELETE). En los siguientes apartados veremos uno a uno la
implementacin de estos botones.
Insertar un nuevo cliente
Como ya comentamos en el artculo anterior, la insercin de un nuevo cliente la
realizaremos a travs de la siguiente URL:
http://10.0.2.2:2731/Api/Clientes/Cliente
Utilizaremos la accin http POST y tendremos que incluir en la peticin un objeto en
formato JSON que contenga los datos del nuevo cliente (tan slo Nombre y Telfono, ya
que el ID se calcular automticamente). El formato de este objeto de entrada ser anlogo
al siguiente:
{Nombre:cccc, Telefono:12345678}
337
Pues bien, para conseguir esto comenzaremos por crear un nuevo objeto HttpClient, que
ser el encargado de realizar la comunicacin HTTP con el servidor a partir de los datos
que nosotros le proporcionemos. Tras esto crearemos la peticin POST creando un nuevo
objeto HttpPost e indicando la URL de llamada al servicio. Modificaremos
mediante setHeader() el atributo http content-type para indicar que el formato de los datos
que utilizaremos en la comunicacin, que como ya indicamos ser JSON (cuyo MIMEType correspondiente es application/json).
1 HttpClient httpClient = new DefaultHttpClient();
2
3 HttpPost post =
4
new HttpPost("http://10.0.2.2:2731/Api/Clientes/Cliente");
5
6 post.setHeader("content-type", "application/json");
El siguiente paso ser crear el objeto JSON a incluir con la peticin, que deber contener
los datos del nuevo cliente a insertar. Para ello creamos un nuevo objeto JSONObject y le
aadimos mediante el mtodoput() los dos atributos necesarios (nombre y telfono) con sus
valores correspondientes, que los obtenemos de los cuadros de texto de la interfaz,
llamados txtNombre y txtTelefono.
Por ltimo asociaremos este objeto JSON a nuestra peticin HTTP convirtindolo primero
al tipoStringEntity e incluyndolo finalmente en la peticin mediante el mtodo setEntity().
1 //Construimos el objeto cliente en formato JSON
2 JSONObject dato = new JSONObject();
3
4 dato.put("Nombre", txtNombre.getText().toString());
5 dato.put("Telefono", Integer.parseInt(txtTelefono.getText().toString()));
6
7 StringEntity entity = new StringEntity(dato.toString());
8 post.setEntity(entity);
Una vez creada nuestra peticin HTTP y asociado el dato de entrada, tan slo nos queda
realizar la llamada al servicio mediante el mtodo execute() del objeto HttpClient y
recuperar el resultado mediante getEntity(). Este resultado lo recibimos en forma de objeto
HttpEntity, pero lo podemos convertir fcilmente en una cadena de texto mediante el
mtodo esttico EntityUtils.toString().
1 HttpResponse resp = httpClient.execute(post);
2 String respStr = EntityUtils.toString(resp.getEntity());
3
4 if(respStr.equals("true"))
5
lblResultado.setText("Insertado OK.");
En nuestro caso, el mtodo de insercin devuelve nicamente un valor booleano indicando
si el registro se ha insertado correctamente en la base de datos, por lo que tan slo
338
tendremos que verificar el valor de este booleano (true o false) para conocer el
resultado de la operacin, que mostraremos en la interfaz en una etiqueta de texto
llamada lblResultado.
Como ya dijimos en el apartado anterior sobre servicios SOAP, a partir de la versin 3 de
Android no se permite realizar operaciones de larga duracin dentro del hilo principal de la
aplicacin, entre ellas conexiones a internet como estamos haciendo en esta ocasin. Para
solucionar este problema y que la aplicacin funcione correctamente en todas las versiones
de Android debemos hacer la llamada al servicio mediante una tarea asncrona,
o AsynTask, que se ejecute en segundo plano. Una vez ms no entrar en detalles porque el
curso contiene un captulo dedicado exclusivamente a este tema. A modo de ejemplo, el
cdigo anterior trasladado a una AsyncTask quedara de la siguiente forma:
1
private class TareaWSInsertar extends AsyncTask<String,Integer,Boolean> {
2
3
protected Boolean doInBackground(String... params) {
4
5
boolean resul = true;
6
7
HttpClient httpClient = new DefaultHttpClient();
8
9
HttpPost post = new
10
HttpPost("http://10.0.2.2:2731/Api/Clientes/Cliente");
11
12
post.setHeader("content-type", "application/json");
13
14
try
15
{
16
//Construimos el objeto cliente en formato JSON
17
JSONObject dato = new JSONObject();
18
19
dato.put("Nombre", params[0]);
20
dato.put("Telefono", Integer.parseInt(params[1]));
21
22
StringEntity entity = new StringEntity(dato.toString());
23
post.setEntity(entity);
24
25
HttpResponse resp = httpClient.execute(post);
26
String respStr = EntityUtils.toString(resp.getEntity());
27
28
if(!respStr.equals("true"))
29
resul = false;
30
}
31
catch(Exception ex)
32
{
339
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Log.e("ServicioRest","Error!", ex);
resul = false;
}
return resul;
}
protected void onPostExecute(Boolean result) {
if (result)
{
lblResultado.setText("Insertado OK.");
}
}
}
Y la llamada a la tarea asncrona desde el evento onClick del botn de Insertar sera tan
sencilla como sta:
1
btnInsertar.setOnClickListener(new OnClickListener() {
2
3
@Override
4
public void onClick(View v) {
5
TareaWSInsertar tarea = new TareaWSInsertar();
6
tarea.execute(
7
txtNombre.getText().toString(),
8
txtTelefono.getText().toString());
9
}
10 });
Actualizar un cliente existente
La URL utilizada para la actualizacin de clientes ser la misma que la anterior:
http://10.0.2.2:2731/Api/Clientes/Cliente
Pero en este caso, el objeto JSON a enviar como entrada deber contener no slo los
nuevos valores de nombre y telfono sino tambin el ID del cliente a actualizar, por lo que
tendra una estructura anloga a la siguiente:
{Id:123, Nombre:cccc, Telefono:12345678}
Para actualizar el cliente procederemos de una forma muy similar a la ya comentada para la
insercin, con las nicas diferencias de que en este caso la accin HTTP utilizada
ser PUT (objeto HttpPut) y que el objeto JSON de entrada tendr el campo ID adicional.
1
HttpClient httpClient = new DefaultHttpClient();
2
3
HttpPut put = new HttpPut("http://10.0.2.2:2731/Api/Clientes/Cliente");
340
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
put.setHeader("content-type", "application/json");
try
{
//Construimos el objeto cliente en formato JSON
JSONObject dato = new JSONObject();
dato.put("Id", Integer.parseInt(txtId.getText().toString()));
dato.put("Nombre", txtNombre.getText().toString());
dato.put("Telefono", Integer.parseInt(txtTelefono.getText().toString()));
StringEntity entity = new StringEntity(dato.toString());
put.setEntity(entity);
HttpResponse resp = httpClient.execute(put);
String respStr = EntityUtils.toString(resp.getEntity());
if(respStr.equals("true"))
lblResultado.setText("Actualizado OK.");
}
catch(Exception ex)
{
Log.e("ServicioRest","Error!", ex);
}
Eliminacin de un cliente
La eliminacin de un cliente la realizaremos a travs de la URL siguiente:
http://10.0.2.2:2731/Api/Clientes/Cliente/id_cliente
donde id_cliente ser el ID del cliente a eliminar. Adems, utilizaremos la accin
http DELETE (objetoHttpDelete) para identificar la operacin que queremos realizar. En
este caso no ser necesario pasar ningn objeto de entrada junto con la peticin, por lo que
el cdigo quedar an ms sencillo que los dos casos anteriores.
1
HttpClient httpClient = new DefaultHttpClient();
2
3
String id = txtId.getText().toString();
4
5
HttpDelete del =
6
new HttpDelete("http://10.0.2.2:2731/Api/Clientes/Cliente/" + id);
7
8
del.setHeader("content-type", "application/json");
9
10 try
11 {
12
HttpResponse resp = httpClient.execute(del);
341
13
14
15
16
17
18
19
20
21
Como podis ver, al principio del mtodo obtenemos el ID del cliente desde la interfaz de
la aplicacin y lo concatenamos con la URL base para formar la URL completa de llamada
al servicio.
Obtener un cliente
Esta operacin es un poco distinta a las anteriores, ya que en este caso el resultado devuelto
por el servicio ser un objeto JSON y no un valor simple como en los casos anteriores. Al
igual que en el caso de eliminacin de clientes, la URL a utilizar ser del tipo:
http://10.0.2.2:2731/Api/Clientes/Cliente/id_cliente
En este caso utilizaremos un tipo de peticin http GET (objeto HttpGet) y la forma de
realizar la llamada ser anloga a las anteriores. Donde aparecern las diferencias ser a la
hora de tratar el resultado devuelto por el servicio tras llamar al mtodo getEntity(). Lo que
haremos ser crear un nuevo objetoJSONObject a partir del resultado textual de getEntity().
Hecho esto, podremos acceder a los atributos del objeto utilizando para ello los
mtodos get() correspondientes, segn el tipo de cada atributo (getInt(), getString(), etc).
Tras esto mostraremos los datos del cliente recuperado en la etiqueta de resultados de la
interfaz (lblResultados).
1
HttpClient httpClient = new DefaultHttpClient();
2
3
String id = txtId.getText().toString();
4
5
HttpGet del =
6
new HttpGet("http://10.0.2.2:2731/Api/Clientes/Cliente/" + id);
7
8
del.setHeader("content-type", "application/json");
9
10 try
11 {
12
HttpResponse resp = httpClient.execute(del);
13
String respStr = EntityUtils.toString(resp.getEntity());
14
15
JSONObject respJSON = new JSONObject(respStr);
342
16
17
18
19
20
21
22
23
24
25
26
Una vez ms como podis comprobar el cdigo es muy similar al ya visto para el resto de
operaciones.
Obtener listado completo de clientes
Por ltimo vamos a ver cmo podemos obtener el listado completo de clientes. El inters de
esta operacin est en que el resultado recuperado de la llamada al servicio ser un array de
objetos de tipo cliente, por supuesto en formato JSON. La accin http utilizada ser una vez
ms la accin GET, y la URL para recuperar el listado de clientes ser:
http://10.0.2.2:2731/Api/Clientes
De nuevo, la forma de llamar al servicio ser anloga a las anteriores hasta la llamada
a getEntity()para recuperar los resultados. En esta ocasin, dado que recibimos un array de
elementos, convertiremos este resultado a un objeto JSONArray, y hecho esto podremos
acceder a cada uno de los elementos del array mediante una llamada a getJSONObject(), al
que iremos pasando el ndice de cada elemento. Para saber cuntos elementos contiene el
array podremos utilizar el mtodo length() del objeto JSONArray. Por ltimo, el acceso a
los atributos de cada elemento del array lo realizamos exactamente igual como ya lo
hicimos en la operacin anterior de obtencin de cliente por ID.
1
HttpClient httpClient = new DefaultHttpClient();
2
3
HttpGet del =
4
new HttpGet("http://10.0.2.2:2731/Api/Clientes");
5
6
del.setHeader("content-type", "application/json");
7
8
try
9
{
10
HttpResponse resp = httpClient.execute(del);
11
String respStr = EntityUtils.toString(resp.getEntity());
12
13
JSONArray respJSON = new JSONArray(respStr);
14
343
15
String[] clientes = new String[respJSON.length()];
16
17
for(int i=0; i<respJSON.length(); i++)
18
{
19
JSONObject obj = respJSON.getJSONObject(i);
20
21
int idCli = obj.getInt("Id");
22
String nombCli = obj.getString("Nombre");
23
int telefCli = obj.getInt("Telefono");
24
25
clientes[i] = "" + idCli + "-" + nombCli + "-" + telefCli;
26
}
27
28
//Rellenamos la lista con los resultados
29
ArrayAdapter<String> adaptador =
30
new ArrayAdapter<String>(ServicioWebRest.this,
31
android.R.layout.simple_list_item_1, clientes);
32
33
lstClientes.setAdapter(adaptador);
34 }
35 catch(Exception ex)
36 {
37
Log.e("ServicioRest","Error!", ex);
38 }
Tras obtener nuestro array de clientes, para mostrar los resultados hemos aadido a la
interfas de nuestra aplicacin de ejemplo un control tipo ListView (llamado lstClientes) que
hemos rellenado a travs de su adaptador con los datos de los clientes recuperados.
A modo de ejemplo, en la siguiente imagen puede verse el resultado de ejecutar la
operacin de listado completo de clientes:
344
Y con esto hemos terminado. Espero haber ilustrado con claridad en los dos ltimos
artculos la forma de construir servicios web tipo REST mediante ASP.NET y aplicaciones
cliente Android capaces de acceder a dichos servicios.
345
Obviamente, stos son el tipo de errores que nadie quiere ver al utilizar su aplicacin, y en
este artculo y los siguientes vamos a ver varias formas de evitarlo utilizando procesos en
segundo plano para ejecutar las operaciones de larga duracin. En este primer artculo de la
serie nos vamos a centrar en dos de las alternativas ms directas a la hora de ejecutar tareas
en segundo plano en Android:
1. Crear nosotros mismos de forma explcita un nuevo hilo para ejecutar nuestra tarea.
2. Utilizar la clase auxiliar AsyncTask proporcionada por Android.
Mi idea inicial para este artculo era obviar la primera opcin, ya que normalmente la
segunda solucin nos es ms que suficiente, y adems es mas sencilla y ms limpia de
implementar. Sin embargo, si no comentamos al menos de pasada la forma de crear a
mano nuevos hilos y los problemas que surgen, quiz no se viera demasiado claro las
ventajas que tiene utilizar las AsyncTask. Por tanto, finalmente voy a pasar muy
rpidamente por la primera opcin para despus centrarnos un poco ms en la segunda.
Adems, aprovechando el tema de la ejecucin de tareas en segundo plano, vamos a ver
tambin cmo utilizar un control (el ProgressBar) y un tipo de dilogo (el ProgressDialog)
que no vimos en los primeros temas del curso dedicados a la interfaz de usuario.
Y para ir paso a paso, vamso a empezar por crear una aplicacin de ejemplo en cuya
actividad principal colocaremos un control ProgressBar (en mi caso llamado pbarProgreso)
y un botn (btnSinHilos) que ejecute una tarea de larga duracin. Para simular una
operacin de larga duracin vamos a ayudarnos de un mtodo auxiliar que lo nico que
haga sea esperar 1 segundo, mediante una llamada aThread.sleep().
1 private void tareaLarga()
2 {
3
try {
4
Thread.sleep(1000);
5
} catch(InterruptedException e) {}
6 }
Haremos que nuestro botn ejecute este mtodo 10 veces, de forma que nos quedar una
ejecucin de unos 10 segundos en total:
1
btnSinHilos.setOnClickListener(new OnClickListener() {
346
2
@Override
3
public void onClick(View v) {
4
pbarProgreso.setMax(100);
5
pbarProgreso.setProgress(0);
6
7
for(int i=1; i<=10; i++) {
8
tareaLarga();
9
pbarProgreso.incrementProgressBy(10);
10
}
11
12
Toast.makeText(MainHilos.this, "Tarea finalizada!",
13
Toast.LENGTH_SHORT).show();
14
}
15 });
Como veis, aqu todava no estamos utilizando nada especial, por lo que todo el cdigo se
ejecutar en el hilo principal de la aplicacin. En cuanto a la utilizacin del
control ProgressBar vemos que es muy sencilla y no requiere apenas configuracin. En
nuestro caso tan slo hemos establecido el valor mximo que alcanzar (el valor en el que
la barra de progreso estar rellena al mximo) mediante el mtodosetMax(100),
posteriormente la hemos inicializado al valor mnimo mediante una llamada
asetProgress(0) de forma que inicialmente aparezca completamente vaca, y por ltimo en
cada iteracin del bucle incrementamos su valor en 10 unidades llamando
a incrementProgressBy(10), de tal forma que tras la dcima iteracin la barra llegue al valor
mximo de 100 que establecimos antes. Finalmente mostramos un mensaje Toast para
informar de la finalizacin de la tarea. Pues bien, ejecutemos la aplicacin, pulsemos el
botn, y veamos qu ocurre.
He colocado un pequeo vdeo al final del artculo donde puede verse el resultado final de
todas las pruebas que haremos durante este tutorial. En concreto esta primera prueba puede
verse entre los segundos 00:00 00:12
No era eso lo que esperbamos verdad? Lo que ha ocurrido es que desde el momento que
hemos pulsado el botn para ejecutar la tarea, hemos bloqueado completamente el resto de
la aplicacin, incluida la actualizacin de la interfaz de usuario, que ha debido esperar a que
sta termine mostrando directamente la barra de progreso completamente llena. En
definitiva, no hemos sido capaces de ver el progreso de la tarea. Pero como decamos, este
efecto puede empeorar. Probemos ahora a pulsar el botn de la tarea y mientras sta se
ejecuta realicemos cualquier accin sobre la pantalla, un simple click sobre el fondo nos
basta. Veamos qu ocurre ahora.
Puedes verlo en el vdeo entre los segundos 00:13 00:28
347
Vemos cmo al intentar hacer cualquier accin sobre la aplicacin Android nos ha
advertido con un mensaje de error que la aplicacin no responde debido a que se est
ejecutando una operacin de larga duracin en el hilo principal. El usuario debe elegir entre
esperar a que termine de ejecutarla o forzar el cierre de la aplicacin. Pues bien, estos son
los efectos que vamos a intentar evitar. La opcin ms inmediata que nos proporciona
Android, al igual que otras plataformas, es crear directamente hilos secundarios dentro de
los cuales ejecutar nuestras operaciones costosas. Esto lo conseguimos en Android
instanciando un objeto de la clase Thread. El constructor de la clase Thread recibe como
parmetro un nuevo objeto Runnable que debemos construir implementando su
mtodo run(), dentro del cual vamos a realizar nuestra tarea de larga duracin. Hecho esto,
debemos llamar al mtodo start() del objetoThreaddefinido para comenzar la ejecucin de
la tarea en segundo plano.
1 new Thread(new Runnable() {
2
public void run() {
3
//Aqu ejecutamos nuestras tareas costosas
4
}
5 }).start();
Hasta aqu todo sencillo y relativamente limpio. Los problemas aparecen cuando nos damos
cuenta que desde este hilo secundario que hemos creado no podemos hacer referencia
directa a componentes que se ejecuten en el hilo principal, entre ellos los controles que
forman nuestra interfaz de usuario, es decir, que desde el mtodo run() no podramos ir
actualizando directamente nuestra barra de progreso de la misma forma que lo hacamos
antes. Para solucionar esto, Android proporciona varias alternativas, entre ellas la
utilizacin del mtodo post() para actuar sobre cada control de la interfaz, o la llamada al
mtodorunOnUiThread() para enviar operaciones al hilo principal desde el hilo
secundario [Nota: S, vale, s que no he nombrado la opcin de los Handler, pero no quera
complicar ms el tema por el momento]. Ambas opciones requieren como parmetro un
nuevo objeto Runnable del que nuevamente habr que implementar su mtodo run() donde
se acte sobre los elementos de la interfaz. Por ver algn ejemplo, en nuestro caso hemos
utilizado el mtodo post() para actuar sobre el control ProgressBar, y el
mtodorunOnUiThread()para mostrar el mensaje toast.
1
new Thread(new Runnable() {
2
public void run() {
3
pbarProgreso.post(new Runnable() {
4
public void run() {
5
pbarProgreso.setProgress(0);
6
}
7
});
8
348
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Utilicemos este cdigo dentro de un nuevo botn de nuestra aplicacin de ejemplo y vamos
a probarlo en el emulador.
Puedes verlo en el vdeo entre los segundos 00:29 00:43
Ahora s podemos ver el progreso de nuestra tarea reflejado en la barra de progreso. La
creacin de un hilo secundario nos ha permitido mantener el hilo principal libre de forma
que nuestra interfaz de usuario de actualiza sin problemas durante la ejecucin de la tarea
en segundo plano. Sin embargo miremos de nuevo el cdigo anterior. Complicado de leer,
verdad? Y eso considerando que tan slo estamos actualizando un control de nuestra
interfaz. Si el nmero de controles fuera mayor, o necesitramos una mayor interaccin con
la interfaz el cdigo empezara a ser inmanejable, difcil de leer y mantener, y por tanto
tambin ms propenso a errores. Pues bien, aqu es donde Android llega en nuestra ayuda y
nos ofrece la clase AsyncTask, que nos va a permitir realizar esto mismo pero con la
ventaja de no tener que utilizar artefactos del tipo runOnUiThread() y de una forma mucho
ms organizada y legible. La forma bsica de utilizar la clase AsyncTaskconsiste en crear
una nueva clase que extienda de ella y sobrescribir varios de sus mtodos entre los que
repartiremos la funcionalidad de nuestra tarea. Estos mtodos son los siguientes:
onPreExecute(). Se ejecutar antes del cdigo principal de nuestra tarea. Se suele utilizar
para preparar la ejecucin de la tarea, inicializar la interfaz, etc.
doInBackground(). Contendr el cdigo principal de nuestra tarea.
onProgressUpdate().
Se
ejecutar
cada
vez
que
llamemos
al
mtodo publishProgress() desde el mtodo doInBackground().
onPostExecute(). Se ejecutar cuando finalice nuestra tarea, o dicho de otra forma, tras la
finalizacin del mtodo doInBackground().
349
mtodo publishProgress()para
que
automticamente
desde
el
mtodo onProgressUpdate() se actualice la interfaz si es necesario. Al extender una nueva
clase de AsyncTaskindicaremos tres parmetros de tipo:
1. El tipo de datos que recibiremos como entrada de la tarea en el mtodo doInBackground().
2. El tipo de datos con el que actualizaremos el progreso de la tarea, y que recibiremos como
parmetro del mtodo onProgressUpdate() y que a su vez tendremos que incluir como
parmetro del mtodopublishProgress().
3. El tipo de datos que devolveremos como resultado de nuestra tarea, que ser el tipo de
retorno del mtodo doInBackground() y el tipo del parmetro recibido en el
mtodo onPostExecute().
En
nuestro
caso
de
ejemplo,
extenderemos
de AsyncTask indicando
los
350
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
if(isCancelled())
break;
}
return true;
}
@Override
protected void onProgressUpdate(Integer... values) {
int progreso = values[0].intValue();
pbarProgreso.setProgress(progreso);
}
@Override
protected void onPreExecute() {
pbarProgreso.setMax(100);
pbarProgreso.setProgress(0);
}
@Override
protected void onPostExecute(Boolean result) {
if(result)
Toast.makeText(MainHilos.this, "Tarea finalizada!",
Toast.LENGTH_SHORT).show();
}
@Override
protected void onCancelled() {
Toast.makeText(MainHilos.this, "Tarea cancelada!",
Toast.LENGTH_SHORT).show();
}
}
351
ProgressDialog horizontal
ProgressDialog spinner
Lo siguiente ser especificar el texto a mostrar en el dilogo, en nuestro caso el mensaje
Procesando, y el valor mximo de nuestro progreso, que lo mantendremos en 100. Por
ltimo indicaremos si deseamos que el dilogo sea cancelable, es decir, que el usuario
pueda cerrarlo pulsando el botn Atrs del telfono. Para nuestro ejemplo activaremos esta
propiedad para ver cmo podemos cancelar tambin nuestra tarea en segundo plano cuando
el usuario cierra el dilogo. Tras la configuracin del dilogo lanzaremos laAsyncTask del
ejemplo anterior, que tendremos que modificar ligeramente para adaptarla al nuevo dilogo.
Veamos el cdigo por ahora:
1
btnAsyncDialog.setOnClickListener(new OnClickListener() {
352
2
3
@Override
4
public void onClick(View v) {
5
6
pDialog = new ProgressDialog(MainHilos.this);
7
pDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
8
pDialog.setMessage("Procesando...");
9
pDialog.setCancelable(true);
10
pDialog.setMax(100);
11
12
tarea2 = new MiTareaAsincronaDialog();
13
tarea2.execute();
14
}
15 });
La AsyncTask ser muy similar a la que ya implementamos.
De
hecho
el
Integer,
353
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Override
protected void onProgressUpdate(Integer... values) {
int progreso = values[0].intValue();
pDialog.setProgress(progreso);
}
@Override
protected void onPreExecute() {
pDialog.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
MiTareaAsincronaDialog.this.cancel(true);
}
});
pDialog.setProgress(0);
pDialog.show();
}
@Override
protected void onPostExecute(Boolean result) {
if(result)
{
pDialog.dismiss();
Toast.makeText(MainHilos.this, "Tarea finalizada!",
Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onCancelled() {
Toast.makeText(MainHilos.this, "Tarea cancelada!",
Toast.LENGTH_SHORT).show();
}
}
Si ahora ejecutamos nuestro proyecto y pulsamos sobre el ltimo botn incluido veremos
cmo el dilogo aparece por encima de nuestra actividad mostrando el progreso de la tarea
asncrona. Tras finalizar, el dilogo desaparece y se muestra el mensaje toast de
finalizacin. Si en cambio, se pulsa el botn Atrs del dispositivo antes de que la tarea
termine el dilogo se cerrar y se mostrar el mensaje de cancelacin.
Puedes verlo en el vdeo entre los segundos 01:07 01:35
354
Y con esto habramos concluido este primer artculo sobre hilos y tareas en segundo plano.
Os dejo a continuacin el vdeo de demostracin de la aplicacin de ejemplo construida
durante el tema.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
Demo Hilos y AsyncTask en Android
Obviamente, stos son el tipo de errores que nadie quiere ver al utilizar su aplicacin, y en
este artculo y los siguientes vamos a ver varias formas de evitarlo utilizando procesos en
segundo plano para ejecutar las operaciones de larga duracin. En este primer artculo de la
serie nos vamos a centrar en dos de las alternativas ms directas a la hora de ejecutar tareas
en segundo plano en Android:
1. Crear nosotros mismos de forma explcita un nuevo hilo para ejecutar nuestra tarea.
2. Utilizar la clase auxiliar AsyncTask proporcionada por Android.
355
Mi idea inicial para este artculo era obviar la primera opcin, ya que normalmente la
segunda solucin nos es ms que suficiente, y adems es mas sencilla y ms limpia de
implementar. Sin embargo, si no comentamos al menos de pasada la forma de crear a
mano nuevos hilos y los problemas que surgen, quiz no se viera demasiado claro las
ventajas que tiene utilizar las AsyncTask. Por tanto, finalmente voy a pasar muy
rpidamente por la primera opcin para despus centrarnos un poco ms en la segunda.
Adems, aprovechando el tema de la ejecucin de tareas en segundo plano, vamos a ver
tambin cmo utilizar un control (el ProgressBar) y un tipo de dilogo (el ProgressDialog)
que no vimos en los primeros temas del curso dedicados a la interfaz de usuario.
Y para ir paso a paso, vamso a empezar por crear una aplicacin de ejemplo en cuya
actividad principal colocaremos un control ProgressBar (en mi caso llamado pbarProgreso)
y un botn (btnSinHilos) que ejecute una tarea de larga duracin. Para simular una
operacin de larga duracin vamos a ayudarnos de un mtodo auxiliar que lo nico que
haga sea esperar 1 segundo, mediante una llamada aThread.sleep().
1 private void tareaLarga()
2 {
3
try {
4
Thread.sleep(1000);
5
} catch(InterruptedException e) {}
6 }
Haremos que nuestro botn ejecute este mtodo 10 veces, de forma que nos quedar una
ejecucin de unos 10 segundos en total:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
btnSinHilos.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
pbarProgreso.setMax(100);
pbarProgreso.setProgress(0);
for(int i=1; i<=10; i++) {
tareaLarga();
pbarProgreso.incrementProgressBy(10);
}
Toast.makeText(MainHilos.this, "Tarea finalizada!",
Toast.LENGTH_SHORT).show();
}
});
Como veis, aqu todava no estamos utilizando nada especial, por lo que todo el cdigo se
ejecutar en el hilo principal de la aplicacin. En cuanto a la utilizacin del
control ProgressBar vemos que es muy sencilla y no requiere apenas configuracin. En
356
nuestro caso tan slo hemos establecido el valor mximo que alcanzar (el valor en el que
la barra de progreso estar rellena al mximo) mediante el mtodosetMax(100),
posteriormente la hemos inicializado al valor mnimo mediante una llamada
asetProgress(0) de forma que inicialmente aparezca completamente vaca, y por ltimo en
cada iteracin del bucle incrementamos su valor en 10 unidades llamando
a incrementProgressBy(10), de tal forma que tras la dcima iteracin la barra llegue al valor
mximo de 100 que establecimos antes. Finalmente mostramos un mensaje Toast para
informar de la finalizacin de la tarea. Pues bien, ejecutemos la aplicacin, pulsemos el
botn, y veamos qu ocurre.
He colocado un pequeo vdeo al final del artculo donde puede verse el resultado final de
todas las pruebas que haremos durante este tutorial. En concreto esta primera prueba puede
verse entre los segundos 00:00 00:12
No era eso lo que esperbamos verdad? Lo que ha ocurrido es que desde el momento que
hemos pulsado el botn para ejecutar la tarea, hemos bloqueado completamente el resto de
la aplicacin, incluida la actualizacin de la interfaz de usuario, que ha debido esperar a que
sta termine mostrando directamente la barra de progreso completamente llena. En
definitiva, no hemos sido capaces de ver el progreso de la tarea. Pero como decamos, este
efecto puede empeorar. Probemos ahora a pulsar el botn de la tarea y mientras sta se
ejecuta realicemos cualquier accin sobre la pantalla, un simple click sobre el fondo nos
basta. Veamos qu ocurre ahora.
Puedes verlo en el vdeo entre los segundos 00:13 00:28
Vemos cmo al intentar hacer cualquier accin sobre la aplicacin Android nos ha
advertido con un mensaje de error que la aplicacin no responde debido a que se est
ejecutando una operacin de larga duracin en el hilo principal. El usuario debe elegir entre
esperar a que termine de ejecutarla o forzar el cierre de la aplicacin. Pues bien, estos son
los efectos que vamos a intentar evitar. La opcin ms inmediata que nos proporciona
Android, al igual que otras plataformas, es crear directamente hilos secundarios dentro de
los cuales ejecutar nuestras operaciones costosas. Esto lo conseguimos en Android
instanciando un objeto de la clase Thread. El constructor de la clase Thread recibe como
parmetro un nuevo objeto Runnable que debemos construir implementando su
mtodo run(), dentro del cual vamos a realizar nuestra tarea de larga duracin. Hecho esto,
debemos llamar al mtodo start() del objetoThreaddefinido para comenzar la ejecucin de
la tarea en segundo plano.
1 new Thread(new Runnable() {
2
public void run() {
357
3
4
5
Hasta aqu todo sencillo y relativamente limpio. Los problemas aparecen cuando nos damos
cuenta que desde este hilo secundario que hemos creado no podemos hacer referencia
directa a componentes que se ejecuten en el hilo principal, entre ellos los controles que
forman nuestra interfaz de usuario, es decir, que desde el mtodo run() no podramos ir
actualizando directamente nuestra barra de progreso de la misma forma que lo hacamos
antes. Para solucionar esto, Android proporciona varias alternativas, entre ellas la
utilizacin del mtodo post() para actuar sobre cada control de la interfaz, o la llamada al
mtodorunOnUiThread() para enviar operaciones al hilo principal desde el hilo
secundario [Nota: S, vale, s que no he nombrado la opcin de los Handler, pero no quera
complicar ms el tema por el momento]. Ambas opciones requieren como parmetro un
nuevo objeto Runnable del que nuevamente habr que implementar su mtodo run() donde
se acte sobre los elementos de la interfaz. Por ver algn ejemplo, en nuestro caso hemos
utilizado el mtodo post() para actuar sobre el control ProgressBar, y el
mtodorunOnUiThread()para mostrar el mensaje toast.
1
new Thread(new Runnable() {
2
public void run() {
3
pbarProgreso.post(new Runnable() {
4
public void run() {
5
pbarProgreso.setProgress(0);
6
}
7
});
8
9
for(int i=1; i<=10; i++) {
10
tareaLarga();
11
pbarProgreso.post(new Runnable() {
12
public void run() {
13
pbarProgreso.incrementProgressBy(10);
14
}
15
});
16
}
17
18
runOnUiThread(new Runnable() {
19
public void run() {
20
Toast.makeText(MainHilos.this, "Tarea finalizada!",
21
Toast.LENGTH_SHORT).show();
22
}
23
});
24
}
25 }).start();
358
Utilicemos este cdigo dentro de un nuevo botn de nuestra aplicacin de ejemplo y vamos
a probarlo en el emulador.
Puedes verlo en el vdeo entre los segundos 00:29 00:43
Ahora s podemos ver el progreso de nuestra tarea reflejado en la barra de progreso. La
creacin de un hilo secundario nos ha permitido mantener el hilo principal libre de forma
que nuestra interfaz de usuario de actualiza sin problemas durante la ejecucin de la tarea
en segundo plano. Sin embargo miremos de nuevo el cdigo anterior. Complicado de leer,
verdad? Y eso considerando que tan slo estamos actualizando un control de nuestra
interfaz. Si el nmero de controles fuera mayor, o necesitramos una mayor interaccin con
la interfaz el cdigo empezara a ser inmanejable, difcil de leer y mantener, y por tanto
tambin ms propenso a errores. Pues bien, aqu es donde Android llega en nuestra ayuda y
nos ofrece la clase AsyncTask, que nos va a permitir realizar esto mismo pero con la
ventaja de no tener que utilizar artefactos del tipo runOnUiThread() y de una forma mucho
ms organizada y legible. La forma bsica de utilizar la clase AsyncTaskconsiste en crear
una nueva clase que extienda de ella y sobrescribir varios de sus mtodos entre los que
repartiremos la funcionalidad de nuestra tarea. Estos mtodos son los siguientes:
onPreExecute(). Se ejecutar antes del cdigo principal de nuestra tarea. Se suele utilizar
para preparar la ejecucin de la tarea, inicializar la interfaz, etc.
doInBackground(). Contendr el cdigo principal de nuestra tarea.
onProgressUpdate().
Se
ejecutar
cada
vez
que
llamemos
al
mtodo publishProgress() desde el mtodo doInBackground().
onPostExecute(). Se ejecutar cuando finalice nuestra tarea, o dicho de otra forma, tras la
finalizacin del mtodo doInBackground().
onCancelled(). Se ejecutar cuando se cancele la ejecucin de la tarea antes de su
finalizacin normal.
Estos mtodos tienen una particularidad esencial para nuestros intereses. El
mtodo doInBackground()se ejecuta en un hilo secundario (por tanto no podremos
interactuar con la interfaz), pero sin embargo todos los dems se ejecutan en el hilo
principal, lo que quiere decir que dentro de ellos podremos hacer referencia directa a
nuestros controles de usuario para actualizar la interfaz. Por su parte, dentro
dedoInBackground() tendremos
la
posibilidad
de
llamar peridicamente al
mtodo publishProgress()para
que
automticamente
desde
el
359
3. El tipo de datos que devolveremos como resultado de nuestra tarea, que ser el tipo de
retorno del mtodo doInBackground() y el tipo del parmetro recibido en el
mtodo onPostExecute().
360
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
pbarProgreso.setProgress(0);
}
@Override
protected void onPostExecute(Boolean result) {
if(result)
Toast.makeText(MainHilos.this, "Tarea finalizada!",
Toast.LENGTH_SHORT).show();
}
@Override
protected void onCancelled() {
Toast.makeText(MainHilos.this, "Tarea cancelada!",
Toast.LENGTH_SHORT).show();
}
}
361
ProgressDialog horizontal
ProgressDialog spinner
Lo siguiente ser especificar el texto a mostrar en el dilogo, en nuestro caso el mensaje
Procesando, y el valor mximo de nuestro progreso, que lo mantendremos en 100. Por
ltimo indicaremos si deseamos que el dilogo sea cancelable, es decir, que el usuario
pueda cerrarlo pulsando el botn Atrs del telfono. Para nuestro ejemplo activaremos esta
propiedad para ver cmo podemos cancelar tambin nuestra tarea en segundo plano cuando
el usuario cierra el dilogo. Tras la configuracin del dilogo lanzaremos laAsyncTask del
ejemplo anterior, que tendremos que modificar ligeramente para adaptarla al nuevo dilogo.
Veamos el cdigo por ahora:
1
btnAsyncDialog.setOnClickListener(new OnClickListener() {
2
3
@Override
4
public void onClick(View v) {
5
6
pDialog = new ProgressDialog(MainHilos.this);
7
pDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
8
pDialog.setMessage("Procesando...");
9
pDialog.setCancelable(true);
10
pDialog.setMax(100);
11
12
tarea2 = new MiTareaAsincronaDialog();
13
tarea2.execute();
14
}
15 });
La AsyncTask ser
muy similar
la
que
ya
implementamos.
De
hecho
el
362
363
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
pDialog.setProgress(0);
pDialog.show();
}
@Override
protected void onPostExecute(Boolean result) {
if(result)
{
pDialog.dismiss();
Toast.makeText(MainHilos.this, "Tarea finalizada!",
Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onCancelled() {
Toast.makeText(MainHilos.this, "Tarea cancelada!",
Toast.LENGTH_SHORT).show();
}
}
Si ahora ejecutamos nuestro proyecto y pulsamos sobre el ltimo botn incluido veremos
cmo el dilogo aparece por encima de nuestra actividad mostrando el progreso de la tarea
asncrona. Tras finalizar, el dilogo desaparece y se muestra el mensaje toast de
finalizacin. Si en cambio, se pulsa el botn Atrs del dispositivo antes de que la tarea
termine el dilogo se cerrar y se mostrar el mensaje de cancelacin.
Puedes verlo en el vdeo entre los segundos 01:07 01:35
Y con esto habramos concluido este primer artculo sobre hilos y tareas en segundo plano.
Os dejo a continuacin el vdeo de demostracin de la aplicacin de ejemplo construida
durante el tema.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
Demo Hilos y AsyncTask en Android
Tareas en segundo plano en Android (II): IntentService
by Sgoliver on 05/08/2012 in Android, Programacin
En el artculo anterior del Curso de Programacin Android vimos cmo ejecutar tareas en
segundo plano haciendo uso de hilos (Thread) y tareas asncronas (AsyncTask). En este
nuevo artculo nos vamos a centrar en una alternativa menos conocida, aunque tanto o ms
364
interesante, para conseguir el mismo objetivo: ejecutar una determinada tarea en un hilo
independiente al hilo principal de la aplicacin. Esta opcin se llama IntentService, y no es
ms que un tipo particular de servicio Android que se preocupar por nosotros de la
creacin y gestin del nuevo hilo de ejecucin y de detenerse a s mismo una vez concluida
su tarea asociada.
Como en el caso de las AsyncTask, la utilizacin de un IntentService va a ser tan sencilla
como
extender
una
nueva
clase
de IntentService e
implementar
su
mtodo onHandleIntent(). Este mtodo recibe como parmetro un Intent, que podremos
utilizar para pasar al servicio los datos de entrada necesarios.
A diferencia de las AsyncTask, un IntentService no proporciona mtodos que se ejecuten
en el hilo principal de la aplicacin y que podamos aprovechar para comunicarnos con
nuestra interfaz durante la ejecucin. ste es el motivo principal de que
los IntentService sean una opcin menos utilizada a la hora de ejecutar tareas que requieran
cierta vinculacin con la interfaz de la aplicacin. Sin embargo tampoco es imposible su
uso en este tipo de escenarios ya que podremos utilizar por ejemplo mensajes broadcast (y
por supuesto su BroadcastReceiver asociado capaz de procesar los mensajes) para
comunicar eventos al hilo principal, como por ejemplo la necesidad de actualizar controles
de la interfaz o simplemente para comunicar la finalizacin de la tarea ejecutada. En este
artculo veremos cmo implementar este mtodo para conseguir una aplicacin de ejemplo
similar a la que construimos en el artculo anterior utilizandoAsyncTask.
Empezaremos extendiendo una nueva clase derivada de IntentService, que para ser
originales llamaremos MiIntentService. Lo primero que tendremos que hacer ser
implementar un constructor por defecto. Este constructor lo nico que har ser llamar al
constructor de la clase padre pasndole el nombre de nuestra nueva clase.
A continuacin implementaremos el mtodo onHandleIntent(). Como ya hemos indicado,
este mtodo ser el que contenga el cdigo de la tarea a ejecutar en segundo plano. Para
simular una tarea de larga duracin utilizaremos el mismo bucle que ya vimos en el artculo
anterior con la novedad de que esta vez el nmero de iteraciones ser variable, de forma
que podamos experimentar con cmo pasar datos de entrada a travs del Intent recibido
como parmetro en onHandleIntent(). En nuestro caso de ejemplo pasaremos un slo dato
de entrada que indique el nmero de iteraciones. Por tanto, lo primero que haremos ser
obtener este dato a partir del Intent mediante el mtodo getIntExtra(). Una vez conocemos
el nmero de iteraciones, tan slo nos queda ejecutar el bucle y comunicar el progreso tras
cada iteracin.
Como ya hemos comentado, para comunicar este progreso vamos a hacer uso de
mensajes broadcast. Para enviar este tipo de mensajes necesitamos construir un Intent,
365
366
33
{
34
try {
35
Thread.sleep(1000);
36
} catch(InterruptedException e) {}
37
}
38 }
Como podis comprobar tambin he aadido un nuevo tipo de mensaje broadcast
(ACTION_FIN), esta vez sin datos adicionales, para comunicar a la aplicacin principal la
finalizacin de la tarea en segundo plano.
Adems de la implementacin del servicio, recordemos que tambin tendremos que
declararlo en elAndroidManifest.xml, dentro de la seccin <application>:
1 <service android:name=".MiIntentService"></service>
Y con esto ya tendramos implementado nuestro servicio. El siguiente paso ser llamar al
servicio para comenzar su ejecucin. Esto lo haremos desde una actividad principal de
ejemplo en la que tan slo colocaremos una barra de progreso y un botn para lanzar el
servicio. El cdigo del botn para ejecutar el servicio ser muy sencillo, tan slo tendremos
que crear un nuevo intent asociado a la claseMiIntentService, aadir los datos de entrada
necesarios mediante putExtra() y ejecutar el servicio llamando a startService() pasando
como parmetro el intent de entrada. Como ya dijimos, el nico dato de entrada que
pasaremos ser el nmero de iteraciones a ejecutar.
1 btnEjecutar.setOnClickListener(new OnClickListener() {
2
3
@Override
4
public void onClick(View v) {
5
Intent msgIntent = new Intent(MainActivity.this, MiIntentService.class);
6
msgIntent.putExtra("iteraciones", 10);
7
startService(msgIntent);
8
}
9 });
Con esto ya podramos ejecutar nuestra aplicacin y lanzar la tarea, pero no podramos ver
el progreso de sta ni saber cundo ha terminado porque an no hemos creado
el BroadcastReceiver necesario para capturar los mensajes broadcast que enva el servicio
durante su ejecucin.
Para ello, como clase interna a nuestra actividad principal definiremos una nueva clase que
extienda aBroadcastReceiver y que implemente su mtodo onReceive() para gestionar los
mensajesACTION_PROGRESO y ACTION_FIN que definimos en nuestro IntentService.
En el caso de recibirseACTION_PROGRESO extraeremos el nivel de progreso del intent
recibido y actualizaremos consecuentemente la barra de progreso mediante setProgress().
367
368
Android IntentService
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pgina del curso en GitHub.
1.
2.
3.
4.
5.
Error
Warning
Info
Debug
Verbose
Para cada uno de estos tipos de mensaje existe un mtodo esttico independiente que
permite aadirlo al log de la aplicacin. As, para cada una de las categoras anteriores
tenemos disponibles los mtodos e(),w(), i(), d() y v() respectivamente.
Cada uno de estos mtodos recibe como parmetros la etiqueta (tag) y el texto en s del
mensaje. Como etiqueta de los mensajes, aunque es un campo al que podemos pasar
cualquier valor, suele utilizarse el nombre de la aplicacin o de la actividad concreta que
369
genera el mensaje. Esto nos permitir ms tarde crear filtros personalizados para identificar
y poder visualizar nicamente los mensajes de log que nos interesan, entre todos los
generados por Android [que son muchos] durante la ejecucin de la aplicacin.
Hagamos un miniprograma de ejemplo para ver cmo fuenciona esto. El programa ser tan
simple como aadir varios mensajes de log dentro del mismo onCreate de la actividad
principal y ver qu ocurre. Os muestro el cdigo completo:
public class LogsAndroid extends Activity {
private static final String LOGTAG = "LogsAndroid";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Log.e(LOGTAG, "Mensaje de error");
Log.w(LOGTAG, "Mensaje de warning");
Log.i(LOGTAG, "Mensaje de informacin");
Log.d(LOGTAG, "Mensaje de depuracin");
Log.v(LOGTAG, "Mensaje de verbose");
}
}
Si ejecutamos la aplicacin anterior en el emulador veremos cmo se abre la pantalla
principal que crea Eclipse por defecto y aparentemente no ocurre nada ms. Dnde
podemos ver los mensajes que hemos aadido al log? Pues para ver los mensajes de log nos
tenemos que ir a la perspectiva de Eclipse llamadaDDMS. Una vez en esta perspectiva,
podemos acceder a los mensajes de log en la parte inferior de la pantalla, en una vista
llamada LogCat. En esta ventana se muestran todos los mensajes de log que genera
Android durante la ejecucin de la aplicacin, que son muchos, pero si buscamos un poco
en la lista encontraremos los generados por nuestra aplicacin, tal como se muestra en la
siguiente imagen (click para ampliar):
Como se puede observar, para cada mensaje se muestra toda la informacin que indicamos
al principio del artculo, adems de estar diferenciados por un color distinto segn su
criticidad.
370
En este caso de ejemplo, los mensajes mostrados son pocos y fciles de localizar en el log,
pero para una aplicacin real, el nmero de estos mensajes puede ser mucho mayor y
aparecer intercalados caticamente entre los dems mensajes de Android. Para estos casos,
la ventada de LogCat ofrece una serie de funcionalidades para facilitar la visualizacin y
bsqueda de determinados mensajes.
Por ejemplo, podemos restringir la lista para que slo muestre mensajes con una
determinada criticidad mnima. Esto se consigue pulsando alguno de los 5 primeros botones
que se observan en la parte superior derecha de la ventana de log. As, si por ejemplo
pulsamos sobre el botn de la categora Info (en verde), en la lista slo se mostrarn los
mensajes con criticidad Error, Warning e Info.
Otro mtodo de filtrado ms interesante es la definicin de filtros personalizados (botn
+ verde), donde podemos filtrar la lista para mostrar nicamente los mensajes con un PID
o Tag determinado. Si hemos utilizado como etiqueta de los mensajes el nombre de nuestra
aplicacin o de nuestras actividades esto nos proporcionar una forma sencilla de visualizar
slo los mensajes generados por nuestra aplicacin.
As, para nuestro ejemplo, podramos crear un filtro indicando como Tag la cadena
LogsAndroid, tal como se muestra en la siguiente imagen:
Esto crear una nueva ventana de log con el nombre que hayamos especificado en el filtro,
donde slo aparecern nuestros 5 mensajes de log de ejemplo (click para ampliar):
Por ltimo, cabe mencionar que existe una variante de cada uno de los mtodos de la clase
Log que recibe un parmetro ms en el que podemos pasar un objeto de tipo excepcin.
371
Con esto conseguimos que, adems del mensaje de log indicado, se muestre tambin la
traza de error generada con la excepcin.
Veamos esto con un ejemplo, y para ello vamos a forzar un error de divisin por cero,
vamos a capturar la excepcin y vamos a generar un mensaje de log con la variante
indicada:
try
{
int a = 1/0;
}
catch(Exception ex)
{
Log.e(LOGTAG, "Divisin por cero!", ex);
}
Si volvemos a ejecutar la aplicacin y vemos el log generado, podermos comprobar cmo
se muestra la traza de error corespondiente generada con la excepcin (click para ampliar).
En definitiva, podemos comprobar como la generacin de mensajes de log puede ser una
herramienta sencilla pero muy efectiva a la hora de depurar aplicaciones que no se ajustan
mucho a la depuracin paso a paso, o simplemente para generar trazas de ejecucin de
nuestras aplicaciones para comprobar de una forma sencilla cmo se comportan.
372
incorporado otras nuevas, como la de integracin con Google+ o los famosos Game
Services.
Los Google Play Services viven como una aplicacin ms en todos los dispositivos Android
(versin 2.2 y superiores), lo que nos aporta la ventaja de no tener que preocuparnos de
ellos, ya que son actualizados automticamente por la plataforma cuando existen
novedades. Por decirlo de alguna forma, nosotros tan slo nos tendremos que preocupar de
conectarnos a ellos y utilizar las distintas funcionalidades como si fueran parte de nuestra
propia aplicacin.
En este artculo inicial vamos a ver cmo podemos crear en Eclipse un proyecto capaz de
hacer uso de los Google Play Services. Una vez preparado el proyecto como veremos aqu
cada servicio requerir de pasos adicionales para su utilizacin, por ejemplo el uso de
Google Maps requerir de la obtencin de una clave de acceso que nos permita el uso de su
API. Estos preparativos adicionales los veremos en cada captulo especfico de cada uno de
los servicios.
Empecemos. Cuando queremos hacer uso de cualquiera de las APIs incluidas en los Google
Play Services lo primero que tendremos que hacer ser importar en Eclipse el proyecto de
librera donde se implementan. Este proyecto se puede descargar mediante el SDK
Manager de Android, accediendo a la seccin Extras y marcando el paquete llamado
Google Play Services.
Si observis la captura anterior, veris que tambin existe un paquete llamado Google Play
Services for Froyo. ste ltimo slo deberamos utilizarlo (en sustitucin del primero) si
nuestra aplicacin necesita ejecutarse en dispositivos con Android 2.2, ya que esta versin
de los Play Services probablemente no recibir las futuras actualizaciones de los servicios.
Por tanto, si la versin mnima sobre la que debe ejecutarse nuestra aplicacin es al menos
la 2.3 usaremos siempre el paquete Google Play Services.
Una vez descargado podremos encontrarlo en la siguiente ruta:
373
<ruta-sdk>\extras\google\google_play_services\libproject\google-play-services_lib
Para importarlo en Eclipse utilizaremos la opcin de men File / Import, y
seleccionaremos la opcin Android / Existing Android Code Into Workspace.
374
debemos entrar a las propiedades del proyecto importado (botn derecho / Properties),
accedemos a la seccin Java Build Path y nos aseguramos de que la opcin Android
Private Libraries est marcada en la ventana Order and Export.
375
Por ltimo, aadiremos al final del fichero proguard-project.txt las siguientes lineas para
evitar que esta herramienta elimine algunas clases necesarias:
1
-keep class * extends java.util.ListResourceBundle {
2
protected Object[][] getContents();
3
}
4
5
-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
6
public static final *** NULL;
7
}
8
9
-keepnames @com.google.android.gms.common.annotation.KeepName class *
10 -keepclassmembernames class * {
11
@ccom.google.android.gms.common.annotation.KeepName *;
12 }
13
14 -keepnames class * implements android.os.Parcelable {
15
public static final ** CREATOR;
16 }
Con esto ya tendramos nuestro proyecto preparado para hacer uso de cualquiera de las
APIs incluidas en los Google Play Services. En los artculos dedicados a cada una de estas
APIs haremos siempre referencia a ste para preparar el proyecto y describiremos
posteriormente los pasos adicionales especficos de cada servicio.
para Android. Esta nueva versin presenta muchas novedades interesantes, de las que cabe
destacar las siguientes:
Integracin con los Servicios de Google Play (Google Play Services) y la Consola de APIs.
Utilizacin a travs de un nuevo tipo especfico de fragment (MapFragment), una mejora
muy esperada por muchos.
Utilizacin de mapas vectoriales, lo que repercute en una mayor velocidad de carga y una
mayor eficiencia en cuanto a uso de ancho de banda.
Mejoras en el sistema de cach, lo que reducir en gran medida las famosas reas en blanco
que tardan en cargar.
Los mapas son ahora 3D, es decir, podremos mover nuestro punto de vista de forma que lo
veamos en perspectiva.
376
377
que crear un nuevo proyecto mediante el botn CREATE PROJECT situado en la parte
superior izquierda de la pantalla.
Aparecer entonces una ventana que nos solicitar el nombre e ID del proyecto.
Introducimos algn nombre descriptivo y un ID nico y aceptamos pulsando Create.
Una vez creado el proyecto, accederemos a la opcin APIs & Auth del men izquierdo.
Desde esta ventana podemos activar o desactivar cada una de las APIs de Google que
queremos utilizar. En este caso slo activaremos la llamada Google Maps Android API
v2 pulsando sobre el botn ON/OFF situado justo a su derecha.
Una vez activado accederemos a la opcin Registered Apps del men izquierdo
(submen de APIs & Auth) y pulsaremos el botn REGISTER APP para registrar
nuestra aplicacin.
378
Accediendo a dicha opcin tendremos la posibilidad de obtener nuestra nueva API Key que
nos permita utilizar el servicio de mapas desde nuestra aplicacin particular. Tendremos
que indicar el nombre de la aplicacin, su tipo (Android), el modo de acceso a la API (en
este caso Accessing APIs directly from Android), el paquete java utilizado en la
aplicacin (que en mi caso ser net.sgoliver.android.mapasapi2) y la huella digital SHA1
del certificado con el que firmamos la aplicacin.
Este ltimo dato introducido requiere alguna explicacin. Toda aplicacin Android debe ir
firmada para poder ejecutarse en un dispositivo, tanto fsico como emulado. Este proceso
de firma es uno de los pasos que tenemos que hacer siempre antes de distribuir
pblicamente una aplicacin. Adicionalmentes, durante el desarrollo de la misma, para
realizar las pruebas y la depuracin del cdigo, aunque no seamos conscientes de ello
tambin estamos firmado la aplicacin con un certificado de pruebas.
En las ltimas versiones de Eclipse y el plugin ADT de Android, podemos consultar
directamente la huella SHA1 de nuestro certificado de pruebas accediendo al men
Window /Preferences y entrando a la seccin Android / Build.
379
En caso de disponer de una versin ms antigua que no muestre directamente este dato, lo
que al menos s debe aparecer es la ruta del certificado de pruebas (campo Default debug
keystore). Como se puede observar, en mi caso el certificado de pruebas est en la ruta
C:\Users\Salvador\.android\debug.keystore. Pues bien, si tuviramos que obtener
manualmente nuestra huella digital SHA1 deberemos acceder a dicha ruta desde la consola
de comando de Windows y ejecutar los siguientes comandos:
1
2
3
C:\>cd C:\Users\Salvador\.android\
Una vez rellenos todos los datos de la aplicacin, pulsamos el botn Register y ya
deberamos tener nuestra API Key generada, podremos verla en la pantalla siguiente dentro
del apartado Android Key. Apuntaremos este dato para utilizarlo ms tarde.
380
Con esto ya habramos concluido los preparativos iniciales necesarios para utilizar el
servicio de mapas de Android en nuestras propias aplicaciones, por lo que empecemos a
crear un proyecto de ejemplo en Eclipse.
Abriremos Eclipse y crearemos un nuevo proyecto estandar de Android, en mi caso lo he
llamado android-mapas-api2. Recordemos utilizar para el proyecto el mismo paquete java
que hemos indicado durante la obtencin de la API key.
Tras esto lo primero que haremos ser aadir al fichero AndroidManifest.xml la API Key
que acabamos de generar. Para ello aadiremos al fichero, dentro de la
etiqueta <application>, un nuevo elemento<meta-data> con los siguientes datos:
1 ...
2 <application>
3 ...
4
<meta-data android:name="com.google.android.maps.v2.API_KEY"
5
android:value="api_key"/>
6 ...
7 </application>
Como valor del parmetro android:value tendremos que poner nuestra API Key recien
generada.
Siguiendo con el AndroidManifest, tambin tendremos que incluir una serie de permisos
que nos permitan acceder a internet (INTERNET), conocer el estado de la red
(ACCESS_NETWORK_STATE), acceder al almacenamiento externo del dispositivo para
la cach de mapas (WRITE_EXTERNAL_STORAGE) y hacer uso de los servicios web de
Google (READ_GSERVICES).
381
1
2
3
4
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission
android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
Por ltimo, dado que la API v2 de Google Maps Android utiliza OpenGL ES versin 2,
deberemos especificar tambin dicho requisito en nuestro AndroidManifest aadiendo un
nuevo elemento <uses-feature>:
1 <uses-feature android:glEsVersion="0x00020000"
2
android:required="true"/>
El siguiente paso ser referenciar el proyecto de librera de los Google Play Services desde
nuestro proyecto de ejemplo, si no lo hemos hecho ya. Para ello iremos a sus propiedades
pulsando botn derecho / Properties sobre nuestro proyecto y accediendo a la seccin
Android de las preferencias. En dicha ventana podemos aadir una nueva librera en la
seccin inferior llamada Library. Cuando pulsamos el botn Add nos aparecer la
librera recien importada y podremos seleccionarla directamente, aadindose a nuestra
lista de libreras referenciadas por nuestro proyecto.
382
Las versiones ms recientes de ADT incluyen por defecto esta librera en nuestros
proyectos, pero si no est incluida podis hacerlo mediante la opcin del men contextual
Android Tools / Add Support Library sobre el proyecto, o bien de forma manual.
Y con esto hemos terminado de configurar todo lo necesario. Ya podemos escribir nuestro
cdigo. Y para este primer artculo sobre el tema nos vamos a limitar a mostrar un mapa en
la pantalla principal de la aplicacin. En artculos posteriores veremos como aadir otras
opciones o elementos al mapa.
Para esto tendremos simplemente que aadir el control correspondiente al layout de nuestra
actividad principal. En el caso de la nueva API v2 este control se aadir en forma
de fragment (de ah que hayamos tenido que incluir la librera android-support para poder
utilizarlos en versiones de Android anteriores a la 3.0) de un determinado tipo
(concretamente de la nueva clase com.google.android.gms.maps.SupportMapFragment),
quedando por ejemplo de la siguiente forma:
1 <?xml version="1.0" encoding="utf-8"?>
2 <fragment xmlns:android="http://schemas.android.com/apk/res/android"
3
android:id="@+id/map"
4
android:layout_width="match_parent"
5
android:layout_height="match_parent"
6
class="com.google.android.gms.maps.SupportMapFragment"/>
Por supuesto, dado que estamos utilizando fragments, la actividad principal tambin tendr
que extender aFragmentActivity (en vez de simplemente Activity como es lo normal).
Usaremos tambin la versin de FragmentActivity incluida en la librera androidsupport para ser compatibles con la mayora de las versiones Android actuales.
383
1
2
3
4
5
6
7
8
9
10
Con esto, ya podramos ejecutar y probar nuestra aplicacin. En mi caso las pruebas las he
realizado sobre un dispositivo fsico con Android 2.2 ya que por el momento parece haber
algunos problemas para hacerlo sobre el emulador. Por tanto tendris que conectar vuestro
dispositivo al PC mediante el cable de datos e indicar a Eclipse que lo utilice para la
ejecucin de la aplicacin.
Si ejecutamos el ejemplo deberamos ver un mapa en la pantalla principal de la aplicacin,
sobre el que nos podremos mover y hacer zoom con los gestos habituales o utilizando los
controles de zoom incluidos por defecto sobre el mapa.
Con este artculo espero haber descrito todos los pasos necesarios para comenzar a utilizar
los servicios de mapas de Google utilizando su nueva API Google Maps Android v2. Si
tenis cualquier duda o propuesta de mejora no dudis en escribirlo en los comentarios.
384
Como habis podido comprobar hay muchos preparativos que hacer, aunque ninguno de
ellos de excesiva dificultad. En los prximos artculos aprenderemos a utilizar ms
caractersticas de la nueva API.
385
As, por ejemplo, para modificar el tipo de mapa mostrado podremos utilizar una llamada a
su mtodosetMapType(), pasando como parmetro el tipo de mapa:
MAP_TYPE_NORMAL
MAP_TYPE_HYBRID
MAP_TYPE_SATELLITE
MAP_TYPE_TERRAIN
Para nuestro ejemplo voy a utilizar una variable que almacene el tipo de mapa actual (del 0
al 3) y habilitaremos una opcin de men para ir alternando entre las distintas opciones.
Quedara de la siguiente forma:
1
private void alternarVista()
2
{
3
vista = (vista + 1) % 4;
4
5
switch(vista)
6
{
7
case 0:
8
mapa.setMapType(GoogleMap.MAP_TYPE_NORMAL);
9
break;
10
case 1:
11
mapa.setMapType(GoogleMap.MAP_TYPE_HYBRID);
12
break;
13
case 2:
14
mapa.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
15
break;
16
case 3:
17
mapa.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
18
break;
19
}
20 }
En cuanto al movimiento sobre el mapa, con esta nueva versin de la API vamos a tener
mucha ms libertad que con la anterior versin, ya que podremos mover libremente nuestro
punto de vista (o cmara, como lo han llamado los chicos de Android) por un espacio 3D.
De esta forma, ya no slo podremos hablar de latitud-longitud (target) y zoom, sino
tambin de orientacin (bearing) y ngulo de visin (tilt). La manipulacin de los 2 ltimos
parmetros unida a posibilidad actual de ver edificios en 3D de muchas ciudades nos abren
un mundo de posibilidades.
El movimiento de la cmara se va a realizar siempre mediante la construccin de un
objeto CameraUpdatecon los parmetros necesarios. Para los movimientos ms bsicos
como la actualizacin de la latitud y longitud o el nivel de zoom podremos utilizar la
clase CameraUpdateFactory y sus mtodos estticos que nos facilitar un poco el trabajo.
386
As por ejemplo, para cambiar slo el nivel de zoom podremos utilizar los siguientes
mtodos para crear nuestro CameraUpdate:
CameraUpdateFactory.zoomIn(). Aumenta en 1 el nivel de zoom.
CameraUpdateFactory.zoomOut(). Disminuye en 1 el nivel de zoom.
CameraUpdateFactory.zoomTo(nivel_de_zoom). Establece el nivel de zoom.
Por su parte, para actualizar slo la latitud-longitud de la cmara podremos utilizar:
CameraUpdateFactory.newLatLng(lat, long). Establece la lat-lng expresadas en grados.
Si queremos modificar los dos parmetros anteriores de forma conjunta, tambin tendremos
disponible el mtodo siguiente:
scroll:
CameraUpdateFactory.scrollBy(scrollHorizontal, scrollVertical). Scroll expresado en
pxeles.
Tras construir el objeto CameraUpdate con los parmetros de posicin tendremos que
llamar a los mtodos moveCamera() o animateCamera() de nuestro objeto GoogleMap,
dependiendo de si queremos que la actualizacin de la vista se muestre directamente o de
forma animada.
Con esto en cuenta, si quisiramos por ejemplo centrar la vista en Espaa con un zoom de 5
podramos hacer lo siguiente:
1
2
3
4
CameraUpdate camUpd1 =
CameraUpdateFactory.newLatLng(new LatLng(40.41, -3.69));
mapa.moveCamera(camUpd1);
Adems de los movimientos bsicos que hemos comentado, si queremos modificar los
dems parmetros de la cmara o varios de ellos simultaneamente tendremos disponible el
mtodo ms generalCameraUpdateFactory.newCameraPosition() que recibe como
parmetro un objeto de tipoCameraPosition. Este objeto los construiremos indicando todos
los parmetros de la posicin de la cmara a travs de su mtodo Builder() de la siguiente
forma:
1
LatLng madrid = new LatLng(40.417325, -3.683081);
2
CameraPosition camPos = new CameraPosition.Builder()
3
.target(madrid) //Centramos el mapa en Madrid
4
.zoom(19)
//Establecemos el zoom en 19
5
.bearing(45)
//Establecemos la orientacin con el noreste arriba
6
.tilt(70)
//Bajamos el punto de vista de la cmara 70 grados
7
.build();
8
387
9
10
11
12
CameraUpdate camUpd3 =
CameraUpdateFactory.newCameraPosition(camPos);
mapa.animateCamera(camUpd3);
Como podemos comprobar, mediante este mecanismo podemos modificar todos los
parmetros de posicin de la cmara (o slo algunos de ellos) al mismo tiempo. En nuestro
caso de ejemplo hemos centrado la vista del mapa sobre el parque de El Retiro de Madrid,
con un nivel de zoom de 19, una orientacin de 45 grados para que el noreste est hacia
arriba y un ngulo de visin de 70 grados de forma que veamos en 3D el monumento a
Alfonso XII en la vista de mapa NORMAL. En la siguiente imagen vemos el resultado:
Como podis ver, en esta nueva versin de la API se facilita bastante el posicionamiento
dentro del mapa, y el uso de las clases CameraUpdate y CameraPosition resulta bastante
intuitivo.
Bien, pues ya hemos hablado de cmo modificar nuestro punto de vista sobre el mapa, pero
si el usuario se mueve de forma manual por l, cmo podemos conocer en un momento
dado la posicin de la cmara?
Pues igual de fcil, mediante el mtodo getCameraPosition(), que nos devuelve un
objetoCameraPosition como el que ya conocamos. Accediendo a los distintos mtodos y
propiedades de este objeto podemos conocer con exactitud la posicin de la cmara, la
orientacin y el nivel de zoom.
1 CameraPosition camPos = mapa.getCameraPosition();
2
3 LatLng coordenadas = camPos.target;
388
389
390
12
13
14
Toast.LENGTH_SHORT).show();
}
});
As, cuando el usuario hiciera una pulsacin larga sobre el mapa veramos los datos en
pantalla
de
la
siguiente
forma:
Tambin podremos capturar el evento de cambio de cmara, de forma que podamos realizar
determinadas acciones cada vez que el usuario se mueve manualmente por el mapa,
desplazndolo, haciendo zoom, o modificando la orientacin o el ngulo de visin. Este
evento lo asignaremos al mapa mediante su mtodosetOnCameraChangeListener() y
sobrescribiendo el mtodo onCameraChange(). Este mtodo recibe como parmetro un
objeto CameraPosition, que ya vimos en el artculo anterior, por lo que podremos recuperar
de l todos los datos de la cmara en cualquier momento.
De esta forma, si quisiramos mostrar un Toast con todos los datos podramos hacer lo
siguiente:
1
2
3
4
5
6
7
8
9
10
11
12
mapa.setOnCameraChangeListener(new OnCameraChangeListener() {
public void onCameraChange(CameraPosition position) {
Toast.makeText(
MainActivity.this,
"Cambio Cmara\n" +
"Lat: " + position.target.latitude + "\n" +
"Lng: " + position.target.longitude + "\n" +
"Zoom: " + position.zoom + "\n" +
"Orientacin: " + position.bearing + "\n" +
"ngulo: " + position.tilt,
Toast.LENGTH_SHORT).show();
}
391
13 });
Hecho esto, cada vez que el usuario se mueva por el mapa veramos lo siguiente:
El siguiente tema importante que quera tratar en este artculo es el de los marcadores. Rara
es la aplicacin Android que hace uso de mapas sin utilizar tambin este tipo de elementos
para resaltar determinados puntos en el mapa. Si recordamos el artculo sobre la API v1,
vimos cmo podamos aadir marcadores aadiendo una nueva capa (overlay) al mapa y
dibujando nuestro marcador como parte de su evento draw(). En la nueva versin de la API
tendemos toda esta funcionalidad integrada en la propia vista de mapa, y agregar un
marcador resulta tan sencillo como llamar al mtodo addMarker() pasndole la posicin en
forma de objeto LatLng y el texto a mostrar en la ventana de informacin del marcador. En
nuestra aplicacin de ejemplo aadiremos un men de forma que cuando lo pulsemos se
aada automticamente un marcador sobre Espaa con el texto Pais: Espaa. Veamos
cmo escribir un mtodo auxiliar que nos ayuda a hacer esto pasndole las coordenadas de
latitud y longitud:
1 private void mostrarMarcador(double lat, double lng)
2 {
3
mapa.addMarker(new MarkerOptions()
4
.position(new LatLng(lat, lng))
5
.title("Pais: Espaa"));
6 }
As de sencillo, basta con llamar al mtodo addMarker() pasando como parmetro un nuevo
objetoMarkerOptions sobre el que establecemos la posicin del marcador
(mtodo position()) y el texto a incluir en la ventana de informacin del marcador
(mtodos title() para el ttulo y snippet() para el resto del texto). Si ejecutamos la aplicacin
392
393
394
4
5
PolylineOptions lineas = new PolylineOptions()
6
.add(new LatLng(45.0, -12.0))
7
.add(new LatLng(45.0, 5.0))
8
.add(new LatLng(34.5, 5.0))
9
.add(new LatLng(34.5, -12.0))
10
.add(new LatLng(45.0, -12.0));
11
12
lineas.width(8);
13
lineas.color(Color.RED);
14
15
mapa.addPolyline(lineas);
16 }
Ejecutando esta accin en el emulador veramos lo siguiente:
Pues bien, esto mismo podramos haberlo logrado mediante el dibujo de polgonos, cuyo
funcionamiento es muy similar. Para ello crearamos un nuevo objeto PolygonOptions y
aadiremos las coordenadas de sus puntos en el sentido de las agujas del reloj. En este caso
no es necesario cerrar el circuito (es decir, que la primera coordenada y la ltima fueran
iguales) ya que se hace de forma automtica. Otra diferencia es que para polgonos el ancho
y color de la linea los estableceramos mediante los mtodosstrokeWidth() y strokeColor().
Adems, el dibujo final del polgono sobre el mapa lo haramos mediante addPolygon(). En
nuestro caso quedara como sigue:
1
//Dibujo con polgonos
2
3
PolygonOptions rectangulo = new PolygonOptions()
4
.add(new LatLng(45.0, -12.0),
5
new LatLng(45.0, 5.0),
395
6
new LatLng(34.5, 5.0),
7
new LatLng(34.5, -12.0),
8
new LatLng(45.0, -12.0));
9
10 rectangulo.strokeWidth(8);
11 rectangulo.strokeColor(Color.RED);
12
13 mapa.addPolygon(rectangulo);
El resultado al ejecutar la accin en el emulador debera ser exactamente igual que el
anterior.
Puedes consultar y/o descargar el cdigo completo de los ejemplos desarrollados en este
artculo accediendo a la pagina del curso en GitHub.
Y con esto habramos concluido la serie de tres artculos destinados a describir el
funcionamiento bsico de la nueva API v2 de Google Maps para Android. Espero haber
aclarado las dudas principales a la hora de comenzar a utilizar la nueva API. Tampoco
descarto algn artculo adicional para comentar temas algo ms avanzados sobre esta API,
pero eso ser ms adelante.
396
inmediatamente cualquier nuevo evento a nuestra aplicacin. Esta tcnica, aunque es viable
y efectiva, requiere de muchos recursos abiertos constantemente en nuestro dispositivo,
aumentando por tanto el consumo de CPU, memoria y datos de red necesarios para ejecutar
la aplicacin. Otra solucin utilizada habitualmente sera hacer que nuestra aplicacin
mvil revise de forma peridica en el servidor si existe alguna novedad que notificar al
usuario. Esto se denomina polling, y requiere muchos menos recursos que la opcin
anterior, pero tiene un inconveniente que puede ser importante segn el objetivo de nuestra
aplicacin: cualquier evento que se produzca en el servidor no se notificar al usuario hasta
la prxima consulta al servidor que haga la aplicacin cliente, que podra ser mucho tiempo
despus.
Para solventar este problema Google introdujo en Android, a partir de la versin 2.2
(Froyo), la posibilidad de implementar notificaciones de tipo push, lo que significa que es
el servidor el que inicia el proceso de notificacin, pudiendo realizarla en el mismo
momento que se produce el evento, y el cliente se limita a esperar los mensaje sin tener
que estar periodicamente consultando al servidor para ver si existen novedades, y sin tener
que mantener una conexin permanentemente abierta con ste. En la arquitectura de
Google, todo esto se consigue introduciendo un nuevo actor en el proceso, un servidor de
mensajerapush o cloud to device (que se traducira en algo as como mensajes de la nube
al dispositivo), que se situara entre la aplicacin web y la aplicacin mvil. Este servidor
intermedio se encargar de recibir las notificaciones enviadas desde las aplicaciones web y
hacerlas llegar a las aplicaciones mviles instaladas en los dispositivos correspondientes.
Para ello, deber conocer la existencia de ambas aplicaciones, lo que se consigue mediante
un protocolo bien definido de registros y autorizaciones entre los distintos actores que
participan en el proceso. Veremos ms adelante cmo implementar este proceso.
Este servicio de Google recibi en sus comienzos las siglas C2DM (Cloud to Device
Messaging), pero coincidiendo con su salida de fase beta modific su nombre
a GCM (Google Cloud Messaging).
Lo primero que debemos comentar es la forma de darnos de alta en el servicio, que a pesar
de ser gratuito requiere de un proceso previo de registro y la generacin de una ApiKey,
algo similar a lo que ya vimos al hablar de la utilizacin de mapas en Android. Para hacer
esto debemos acceder a la consola de APIs de Google, en siguiente direccin:
https://code.google.com/apis/console
Suponiendo que es la primera vez que accedemos a esta consola, nos aparecer la pantalla
de bienvenida y la opcin de crear un nuevo proyecto.
397
398
posteriormente como identificacin de acceso. Para ello accedemos al men Api Access
y pulsaremos sobre el botn Create new Server Key.
399
Como hemos indicado antes, para asegurar la correcta comunicacin entre los tres sistemas
se hace necesario un protocolo de registros y autorizaciones que proporcione seguridad y
calidad a todo el proceso. Este proceso se resume en el siguiente grfico (click para
ampliar), que intentar explicar a continuacin.
400
registro, ya que es posible que el servidor GCM invalide peridicamente este ID, genere
uno nuevo y lo vuelva a notificar a la aplicacin cliente.
Este nuevo paso consiste en enviar, desde la aplicacin cliente a la aplicacin
servidor, el cdigo de registro GCM recibido, el cual har las veces de identificador nico
del cliente en el servidor de forma que ste pueda indicar ms tarde el dispositivo mvil
concreto al que desea enviar un mensaje. La aplicacin servidora tendr que ser capaz por
tanto de almacenar y mantener todos los ID de registro de los distintos dispositivos mviles
que se registren como clientes capaces de recibir mensajes.
El ltimo paso ser obviamente el envo en s de un mensaje desde el servidor hasta
un cliente determinado, algo que se har a travs del servidor GCM (paso 4.1) y desde ste
se dirigir al cliente concreto que debe recibirlo (paso 4.2).
Como se puede comprobar, el procedimiento es relativamente complejo, aunque bastante
intuitivo. En los prximos artculos veremos cmo implementar cada uno de ellos. Una vez
ms, para nuestro ejemplo utilizaremos una aplicacin ASP.NET como aplicacin
servidora, con SQL Server a modo de almacn de datos, y aprovechando que ya hemos
visto como crear servicios web SOAP y llamarlos desde aplicaciones Android, vamos a
utilizar uno de stos para la comunicacin entre cliente y servidor (paso 3).
Cloud
Messaging
(GCM).
401
haremos mediante la creacin de un servicio web que exponga un mtodo capaz de recoger
y almacenar los datos de registro de un cliente. La aplicacin Android se conectar
directamente a este servicio web y llamar al mtodo con sus datos identificativos para
registrarse como cliente capaz de recibir notificaciones. Por supuesto que para esto se
podra utilizar cualquier otro mecanismo distinto a servicios web, por ejemplo una simple
peticin HTTP al servidor pasando los datos como parmetros, pero no nos vendr mal para
seguir practicando con servicios web en android, que en este caso ser de tipo SOAP.
Por su lado, el punto 2 lo resolveremos a modo de ejemplo con una pgina web sencilla en
la que podamos indicar el nombre de usuario de cualquiera de los dispositivos registrados
en la base de datos y enviar un mensaje de prueba a dicho cliente.
Vamos a empezar creando la base de datos, aunque no nos detendremos mucho porque ya
vimos el procedimiento por ejemplo en el primer artculo dedicado a servicios web SOAP.
Tan slo decir que crearemos una nueva base de datos llamada DBUSUARIOS, que tendr
dos campos: NombreUsuario yCodigoC2DM, el primero de ellos destinado a almacenar un
nombre de usuario identificativo de cada cliente registrado, y el segundo para almacenar
el RegistrationID de GCM recibido desde dicho cliente a travs del servicio web
(recomiendo consultar el artculo anterior para entender bien todo este protocolo
requerido por GCM).
Una vez creada la base de datos vamos a crear en Visual Studio 2010 un nuevo proyecto C#
de tipo ASP.NET Web Application al que llamaremos GCMServer, y aadiremos a este
proyecto
un
nuevo
componente
de
tipo
Web
Service
llamado
ServicioRegistroGCM.asmx. Todo este procedimiento tambin se puede consultar en el
artculo sobre servicios web SOAP en Android.
Aadiremos un slo mtodo web al servicio, al que llamaremos RegistroCliente() y que
402
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (cod == null)
sql = "INSERT INTO Usuarios (NombreUsuario, CodigoC2DM) VALUES (@usuario, @codigo)"
else
sql = "UPDATE Usuarios SET CodigoC2DM = @codigo WHERE NombreUsuario = @usuario";
SqlCommand cmd = new SqlCommand(sql, con);
cmd.Parameters.Add("@usuario", System.Data.SqlDbType.NVarChar).Value = usuario;
cmd.Parameters.Add("@codigo", System.Data.SqlDbType.NVarChar).Value = regGCM;
res = cmd.ExecuteNonQuery();
con.Close();
return res;
}
El cdigo es sencillo, pero por qu es necesario considerar el caso del UPDATE? Como ya
advertimos en el artculo anterior, el servidor GCM puede en ocasiones refrescar
(actualizar) el ID de registro de un cliente comunicndoselo de nuevo a ste, por lo que a su
vez la aplicacin cliente tendr que hacer tambin la misma actualizacin contra la
aplicacin web. Para ello, el cliente simplemente volver a llamar al
mtodo RegistroCliente() del servicio web pasando el mismo nombre de usuario pero con
el ID de registro actualizado. Para saber si el cliente est ya registrado o no el mtodo se
apoya en un mtodo auxiliar llamado CodigoCliente() que realiza una bsqueda de un
nombre de usuario en la base de datos para devolver su ID de registro en caso de
403
13
14
15
16
17
18
19
20
Con esto ya tendramos implementado nuestro servicio web para el registro de clientes.
Para el envo de los mensajes utilizaremos directamente la pgina Default.aspx creada
por defecto al generar el proyecto de Visual Studio. Modificaremos esta pgina para aadir
tan slo un cuadro de texto donde podamos introducir el nombre de usuario asociado al
cliente al que queremos enviar un mensaje, un botn Enviar con el realizar el envo, y
una etiqueta donde mostrar el estado del envo. Quedara algo como lo siguiente:
404
enviar el mensaje. A parte de ste tambin podemos incluir los siguientes parmetros
opcionales:
delay_while_idle. Hace que el servidor de GCM no enve el mensaje al dispositivo mientras
ste no se encuentre activo.
time_to_live. Indica el tiempo mximo que el mensaje puede permanecer en el servidor de
GCM sin entregar mientras el dispositivo est offline. Por defecto 4 semanas. Si se
especifica algn valor tambin habr que incluir el parmetro siguiente, collapse_key.
collapse_key. ste lo explicar con un ejemplo. Imaginad que activamos el
parmetrodelay_while_idle y que el dispositivo que debe recibir el mensaje permanece
inactivo varias horas. Si durante esas horas se generaran varias notificaciones hacia el
dispositivo, estos mensajes se iran acumulando en el servidor de GCM y cuando el
dispositivo se activara le llegaran todos de golpe. Esto puede tener sentido si cada mensaje
contiene informacin distinta y relevante, pero y si los mensajes simplemente fueran por
ejemplo para decirle al dispositivo Tienes correo nuevo? Sera absurdo entregar en el
varias notificaciones de este tipo en el mismo instante. Pues bien, para esto se utiliza el
parmetro collapse_key. A este parmetro podemos asignarle como valor cualquier cadena
de caracteres, de forma que si se acumulan en el servidor de GCM varios mensajes para el
405
406
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Como vemos el mtodo recibe directamente como parmetro el Registration ID del cliente
al que se va a enviar el mensaje. En primer lugar configuro todos los parmetros que pasar
en la llamada a la API, que en este caso de ejemplo tan slo sern, adems
del registration_id ya comentado, el colapse_key, y una dato adicional que llamar
data.msg (recordemos el prefijo data. obligatorio para este tipo de datos adicionales)
con un mensaje de prueba que contenga la fecha/hora actual. Toda la cadena con estos
407
408
409
410
Lo siguiente ser configurar nuestro AndroidManifest. Lo primero que aadiremos ser una
clusula <uses-sdk> para indicar como versin mnima del SDK soportada la 8 (Android
2.2). Con esto nos aseguraremos de que la aplicacin no se instala en dispositivos con
versin de Android anterior, no soportadas por el servicio GCM.
1
2
3
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="16" />
<permission
android:name="net.sgoliver.android.permission.C2D_MES
android:protectionLevel="signature" />
<uses-permission android:name="net.sgoliver.android.permission.C2D_MESSAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
Los dos primeros aseguran que slo esta aplicacin podr recibir los mensajes, el segundo
permite la recepcin en s de mensajes desde GCM (sustituir mi paquete java
net.sgoliver.android por el vuestro propio en estas lineas), el tercero es el permiso para
poder conectarnos a internet y el ltimo es necesario para tareas realizadas durante la
recepcin de mensajes que veremos ms adelante.
Por ltimo, como componentes de la aplicacin, adems de la actividad principal ya
aadida
por
defecto,
deberemos
declarar
un broadcast
receiver llamado GCMBroadcastReceiver, que no tendremos que crearlo porque ya viene
implementado dentro de la librera gcm.jar (solo tenis que modificar el
elemento<category> para
indicar
vuestro
paquete
java),
y
un
servicio
llamado GCMIntentService (Atencin, es obligatorio este nombre exacto para el servicio si
no queremos tener que implementar nosotros mismos el broadcast receiver anterior). Ya
veremos ms adelante para qu son estos dos componentes.
1
<application
2
android:icon="@drawable/ic_launcher"
3
android:label="@string/app_name"
4
android:theme="@style/AppTheme" >
5
6
...
7
8
<receiver android:name="com.google.android.gcm.GCMBroadcastReceiver"
9
android:permission="com.google.android.c2dm.permission.SEND" >
10
<intent-filter>
11
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
411
12
13
14
15
16
17
18
19
<action android:name="com.google.android.c2dm.intent.REGISTRATION"
/>
<category android:name="net.sgoliver.android" />
</intent-filter>
</receiver>
<service android:name=".GCMIntentService" />
</application>
Una vez definido nuestro AndroidManifest con todos los elementos necesarios vamos a
empezar a implementar la funcionalidad de nuestra aplicacin de ejemplo.
Empezamos por la ms sencilla, el botn de guardar el nombre de usuario. Como
comentamos anteriormente, vamos a utilizar preferencias compartidas para esta tarea.
Como ste es un tema ya vistoen el curso no me detendr en el cdigo ya que es bastante
directo.
1
btnGuardarUsuario.setOnClickListener(new OnClickListener() {
2
@Override
3
public void onClick(View v) {
4
SharedPreferences prefs =
5
getSharedPreferences("MisPreferencias", Context.MODE_PRIVATE);
6
7
SharedPreferences.Editor editor = prefs.edit();
8
editor.putString("usuario", txtUsuario.getText().toString());
9
editor.commit();
10
}
11 });
Como podis comprobar nos limitamos a almacenar una nueva propiedad llamada
usuario con el texto introducido en el cuadro de texto de la interfaz.
El siguiente botn es el de registro del cliente en GCM, y aqu s nos detendremos un poco
para comentar primero cmo funciona internamente este procedimiento.
La aplicacin android debe solicitar el registro en el servicio GCM mediante una peticin
HTTP POST similar a la que ya vimos en el artculo anterior para la aplicacin web. Por
suerte, este procedimiento se ve simplificado enormemente con el uso de la librera gcm.jar,
ya que el montaje y ejecucin de esta peticin queda encapsulado como una simple llamada
a un mtodo esttico de la clase GCMRegistrar, definida en la librera. Por su parte, tanto la
respuesta a esta peticin de registro como la posterior recepcin de mensajes se reciben en
la aplicacin Android en forma de intents. Y aqu es donde entran en juego los dos
componentes que hemos definido anteriormente en nuestro AndroidManifest. El
412
413
9
10
11
12
13
} else {
Log.v("GCMTest", "Ya des-registrado");
}
}
});
Ahora toca procesar las respuestas. Como hemos dicho, para hacer esto tendremos que
implementar el servicio GCMIntentService. Pero no lo haremos desde cero, ya que la
librera de GCM nos proporciona una clase base GCMBaseIntentService de la cual
podemos extender la nuestra, con la ventaja de que tan slo tendremos que sobrescribir
unos pocos mtodos a modo de callbacks, uno por cada posible respuesta o mensaje que
podemos recibir desde el servicio GCM. Estos mtodos son:
onRegistered(context, regId). Se llamar al recibirse una respuesta correcta a la peticin de
registro e incluye como parmetro el Registration ID asignado a nuestro cliente.
onUnregistered(context, regId). Anlogo al anterior pero aplicado a una peticin de desregistro.
onError(context, errorId). Se llamar al recibirse una respuesta de error a una peticin de
registro o des-registro. El cdigo de error concreto se recibe como parmetro.
onMessage(context, intent). Se llamar cada vez que se reciba un nuevo mensaje desde el
servidor de GCM. El contenido del mensaje se recibe en forma de intent, el cual veremos
ms adelante cmo procesar.
Empecemos por el mtodo onRegistered(). Al recibir una respuesta satisfactoria a la
peticin de registro recuperaremos el nombre de usuario almacenado y junto con
el Registration ID recibido nos conectaremos al servicio web que creamos en el artculo
pasado pasndole dichos datos. Esto completar el registro tanto con el servidor de GCM
como con nuestra aplicacin web.
protected void onRegistered(Context context, String regId) {
1
Log.d("GCMTest", "REGISTRATION: Registrado OK.");
2
3
SharedPreferences prefs =
4
context.getSharedPreferences("MisPreferencias",
5
Context.MODE_PRIVATE);
6
7
String usuario = prefs.getString("usuario", "por_defecto");
8
9
registroServidor(usuario, regId);
10
}
El mtodo registroServidor() ser el encargado de realizar la conexin al servicio web y de
la llamada al mtodo web de registro. No me detendr en comentar el cdigo de este
mtodo porque es anlogo a los ejemplos ya vistos en el artculo que dedicamos a servicios
web SOAP en Android. Veamos tan slo el cdigo:
1
private void registroServidor(String usuario, String regId)
2
{
414
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
protected void onUnregistered(Context context, String regId) {
Log.d("GCMTest", "REGISTRATION: Desregistrado OK.");
}
@Override
protected void onError(Context context, String errorId) {
Log.d("GCMTest", "REGISTRATION: Error -> " + errorId);
415
Por ltimo, en el mtodo onMessage() procesaremos el intent con los datos recibidos en el
mensaje y mostraremos una notificacin en la barra de estado de Android.
El intent recibido contendr un elemento en su coleccin de extras por cada dato adicional
que se haya incluido en la peticin que hizo la aplicacin servidor al enviar el mensaje.
Recordis? Aquellos datos adicionales que haba que preceder con el prefijo data.. Si
hacis memoria, en nuestros mensajes de ejemplo tan slo incluamos un dato llamado
data.msg con un mensaje de prueba. Pues bien, estos datos se recuperarn de la coleccin
de extras del intent llamado al mtodo getString() con el nombre del dato, pero esta vez
eliminando el prefijo data.. Veamos cmo quedara todo esto:
1 @Override
2 protected void onMessage(Context context, Intent intent) {
3
String msg = intent.getExtras().getString("msg");
4
Log.d("GCMTest", "Mensaje: " + msg);
5
mostrarNotificacion(context, msg);
6 }
Simple, no?. Al final del mtodo llamamos a un mtodo auxiliar mostrarNotificacion() que
ser el encargado de mostrar la notificacin en la barra de estado de Android. Esto tambin
vimos como hacerlo en detalle en un artculo anterior por lo que tampoco comentaremos el
cdigo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
416
23
24
25
26
27
28
29
30
31
32
33
34
35
Y slo una indicacin ms, adems de sobrescribir estos mtodos en nuestra clase
GCMIntentService, tambin tendremos que aadir un nuevo constructor sin parmetros que
llame directamente al constructor de la clase base pasndole de nuevo el Sender ID que
obtuvimos al crear el nuevo proyecto en la Google API Console. Quedara algo as:
1
2
3
public GCMIntentService() {
super("224338875065");
}
Si no ha quedado claro del todo cmo quedara la clase GCMIntentService completa puede
descargarse y consultarse el cdigo fuente completo al final del artculo.
Y con esto habramos terminado de implementar nuestra aplicacin Android capaz de
recibir mensajes push desde nuestra aplicacin web de ejemplo. Si ejecutamos ambas y
todo ha ido bien, introducimos un nombre de usuario en la aplicacin Android, pulsamos
Aceptar para guardarlo, nos registramos como clientes en GCM pulsando el botn
Registrar GCM, y seguidamente desde la aplicacin web introducimos el mismo nombre
de usuario del cliente, pulsamos el botn Enviar GCM y en breves segundos nos debera
aparecer la notificacin en la barra de estado de nuestro emulador como se observa en las
imgenes siguientes:
417
418
1. Registrarse contra los servidores de GCM como cliente capaz de recibir mensajes.
2. Almacenar el Registration ID recibido como resultado del registro anterior.
3. Comunicar a la aplicacin web el Registration ID de forma que sta pueda enviarle
mensajes.
4. Recibir y procesar los mensajes desde el servidor de GCM.
En la versin anterior de GCM, las tareas 1 y 4 se realizaban normalmente utilizando como
ayuda una librera adicional (gcm.jar) proporcionada por Google. Sin embargo, en la nueva
versin de GCM incluida como parte de los Google Play Services cambian un poco la
filosofa de trabajo y esta librera ya no es necesaria.
Por su parte, el punto 2 lo resolveremos fcilmente mediante el uso de SharedPreferences.
Y por ltimo el punto 3 lo implementaremos mediante la conexin al servicio web SOAP
que creamos en el apartado anterior, sirvindonos para ello de la librera ksoap2, tal como
ya describimos en el captulo sobre servicios web SOAP en Android.
Durante el captulo construiremos una aplicacin de ejemplo muy sencilla, en la que el
usuario podr introducir un nombre de usuario identificativo y pulsar un botn para que
quede guardado en las preferencias de la aplicacin. Tras esto podr registrarse como
cliente capaz de recibir mensajes desde GCM pulsando un botn llamado Registrar. En
caso de realizarse de forma correcta este registro la aplicacin enviar automticamente el
Registration ID recibido y el nombre de usuario almacenado a la aplicacin servidor a
travs del servicio web. Obviamente todo este proceso de registro debera hacerse de forma
transparente para el usuario de una aplicacin real, en esta ocasin he colocado un botn
para ello slo por motivos didcticos y para poder hacer una prueba ms controlada.
Como en el caso de cualquier otro servicio incluido en los Google Play Services el primer
paso para crear nuestra aplicacin Android ser importar el proyecto de librera de los
servicios, crear nuestro propio proyecto y finalmente hacer referencia a la librera desde
nuestro proyecto. Todo este proceso est explicado en el artculo de introduccin a los
Google Play Services.
El siguiente paso ser configurar nuestro AndroidManifest. Lo primero que revisaremos
ser la clusula<usessdk>, donde como versin mnima del SDK debemos indicar la 8
419
(Android 2.2) o superior. Con esto nos aseguraremos de que la aplicacin no se instala en
dispositivos con versin de Android anterior, no soportadas por los Google Play Services.
A continuacin aadiremos los permisos necesarios para ejecutar la aplicacin y utilizar
GCM:
1
2
3
4
5
6
7
8
420
16
17
18
19
20
</receiver>
<service android:name=".GCMIntentService" />
</application>
Una vez definido nuestro AndroidManifest con todos los elementos necesarios vamos a
empezar a implementar la funcionalidad de nuestra aplicacin de ejemplo. Empezaremos
por el proceso de registro que se desencadena al pulsar el botn Registrar de la aplicacin
tras introducir un nombre de usuario.
Nuestro botn de registro tendr que realizar las siguientes acciones:
1. Verificar que el dispositivo tiene instalado Google Play Services.
2. Revisar si ya tenemos almacenado el cdigo de registro de GCM (registration id) de una
ejecucin anterior.
3. Si no disponemos ya del cdigo de registro realizamos un nuevo registro de la aplicacin y
guardamos los datos.
El cdigo del botn con estos tres pasos, que iremos comentando por partes, sera el
siguiente:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
btnRegistrar.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v)
{
context = getApplicationContext();
//Chequemos si est instalado Google Play Services
//if(checkPlayServices())
//{
gcm = GoogleCloudMessaging.getInstance(MainActivity.this);
//Obtenemos el Registration ID guardado
regid = getRegistrationId(context);
//Si no disponemos de Registration ID comenzamos el registro
if (regid.equals("")) {
TareaRegistroGCM tarea = new TareaRegistroGCM();
tarea.execute(txtUsuario.getText().toString());
}
//}
//else
//{
// Log.i(TAG, "No se ha encontrado Google Play Services.");
421
25
26
27
//}
}
});
422
que volver a hacer el registro (salvo en contadas ocasiones que comentaremos ahora). Esta
comprobacin la heremos dentro de un mtodo llamado getRegistrationId(), que entre otras
cosas har uso de preferencias compartidas (Shared Preferences) para recuperar los datos
guardados. Nuestra aplicacin guardar 4 preferencias, que definiremos en nuestra
actividad como constantes:
private static final String PROPERTY_REG_ID = "registration_id";
1
private static final String PROPERTY_APP_VERSION = "appVersion";
2
private
static
final
String
PROPERTY_EXPIRATION_TIME
=
3
"onServerExpirationTimeMs";
4
private static final String PROPERTY_USER = "user";
La primera de ellas es el cdigo de registro de GCM, la segunda guardar la versin de la
aplicacin para la que se ha obtenido dicho cdigo, la tercera indicar la fecha de caducidad
del cdigo de registro guardado, y por ltimo guardaremos el nombre de usuario.
En el mtodo getRegistrationId() lo primero que haremos ser recuperar la
preferenciaPROPERTY_REG_ID. Si sta no est informada saldremos inmediatamente del
mtodo para proceder a un nuevo registro.
Si por el contrario ya tenamos un registration_id guardado podramos seguir utilizndolo
sin tener que registrarnos de nuevo (lo devolveremos como resultado), pero habr tres
situaciones en las que queremos volver a realizar el registro para asegurarnos de que
nuestra aplicacin pueda seguir recibiendo mensajes sin ningn problema:
Si el nombre de usuario ha cambiado.
Si la versin de la aplicacin ha cambiado.
Si se ha sobrepasado la fecha de caducidad del cdigo de registro.
Para verificar esto nuestro mtodo recuperar cada una de las preferencias compartidas,
realizar las verificaciones indicadas y en caso de cumplirse alguna de ellas saldr del
mtodo sin devolver el antiguoregistration_id para que se vuelva a realizar el registro.
1
private String getRegistrationId(Context context)
2
{
3
SharedPreferences prefs = getSharedPreferences(
4
MainActivity.class.getSimpleName(),
5
Context.MODE_PRIVATE);
6
7
String registrationId = prefs.getString(PROPERTY_REG_ID, "");
8
9
if (registrationId.length() == 0)
10
{
11
Log.d(TAG, "Registro GCM no encontrado.");
12
return "";
13
}
14
423
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
String registeredUser =
prefs.getString(PROPERTY_USER, "user");
int registeredVersion =
prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
long expirationTime =
prefs.getLong(PROPERTY_EXPIRATION_TIME, -1);
SimpleDateFormat
sdf =
new
SimpleDateFormat("dd/MM/yyyy
Locale.getDefault());
String expirationDate = sdf.format(new Date(expirationTime));
Log.d(TAG, "Registro GCM encontrado (usuario=" + registeredUser +
", version=" + registeredVersion +
", expira=" + expirationDate + ")");
int currentVersion = getAppVersion(context);
if (registeredVersion != currentVersion)
{
Log.d(TAG, "Nueva versin de la aplicacin.");
return "";
}
else if (System.currentTimeMillis() > expirationTime)
{
Log.d(TAG, "Registro GCM expirado.");
return "";
}
else if (!txtUsuario.getText().toString().equals(registeredUser))
{
Log.d(TAG, "Nuevo nombre de usuario.");
return "";
}
return registrationId;
}
private static int getAppVersion(Context context)
{
try
{
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;
}
HH:mm",
424
62
63
64
65
catch (NameNotFoundException e)
{
throw new RuntimeException("Error al obtener versin: " + e);
}
}
425
27
28
29
30
31
32
33
34
35
36
}
}
catch (IOException ex)
{
Log.d(TAG, "Error registro en GCM:" + ex.getMessage());
}
return msg;
}
}
Lo primero que haremos ser obtener una instancia del servicio de Google Cloud
Messaging mediante el mtodo GoogleCloudMessaging.getInstance(). Obtenido este
objeto, el registro en GCM ser tan sencillo como llamar a su mtodo register() pasndole
como parmetro el Sender ID que obtuvimos al crear el proyecto en la Consola de APIs de
Google. Esta llamada nos devolver el registration_idasignado a nuestra aplicacin.
Tras el registro en GCM debemos tambin registrarnos en nuestro servidor, al que al menos
debemos enviarle nuestro registration_id para que nos pueda enviar mensajes
posteriormente. En nuestro caso de ejemplo, adems del cdigo de registro vamos a
enviarle tambin nuestro nombre de usuario. Como ya dijimos este registro lo vamos a
realizar utilizando el servicio web que creamos en el artculo sobre la parte servidor. La
llamada al servicio web es anloga a las que ya explicamos en el artculo sobre servicios
web SOAP por lo que no entrar en ms detalles, tan slo veamos el cdigo.
1
private boolean registroServidor(String usuario, String regId)
2
{
3
boolean reg = false;
4
5
final String NAMESPACE = "http://sgoliver.net/";
6
final String URL="http://10.0.2.2:1634/ServicioRegistroGCM.asmx";
7
final String METHOD_NAME = "RegistroCliente";
8
final String SOAP_ACTION = "http://sgoliver.net/RegistroCliente";
9
10
SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME);
11
12
request.addProperty("usuario", usuario);
13
request.addProperty("regGCM", regId);
14
15
SoapSerializationEnvelope envelope =
16
new SoapSerializationEnvelope(SoapEnvelope.VER11);
17
18
envelope.dotNet = true;
19
20
envelope.setOutputSoapObject(request);
21
426
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Por ltimo, si todo ha ido bien guardaremos los nuevos datos de registro (usuario,
registration_id, version de la aplicacin y fecha de caducidad) como preferencias
compartidas. Lo haremos todo dentro del mtodosetRegistrationId().
1
private void setRegistrationId(Context context, String user, String regId)
2
{
3
SharedPreferences prefs = getSharedPreferences(
4
MainActivity.class.getSimpleName(),
5
Context.MODE_PRIVATE);
6
7
int appVersion = getAppVersion(context);
8
9
SharedPreferences.Editor editor = prefs.edit();
10
editor.putString(PROPERTY_USER, user);
11
editor.putString(PROPERTY_REG_ID, regId);
12
editor.putInt(PROPERTY_APP_VERSION, appVersion);
13
editor.putLong(PROPERTY_EXPIRATION_TIME,
14
System.currentTimeMillis() + EXPIRATION_TIME_MS);
15
16
editor.commit();
17 }
La forma de guardar los datos mediante preferencias compartidas ya la comentamos en
detalle en elartculo dedicado a las Shared Preferences. Lo nico a comentar es la forma de
calcular la fecha de caducidad del cdigo de registro. Vamos a calcular esa fecha por
427
ejemplo como la actual ms una semana. Para ello obtenemos la fecha actual en
milisegundos
con currentTimeMillis() y
le
sumamos
una
constante EXPIRATION_TIME_MS que hemos definido con el valor 1000 * 3600 * 24 *
7, es decir, los milisegundos de una semana completa.
Y con esto habramos terminado la fase de registro de la aplicacin. Pero para recibir
mensajes an nos faltan dos elementos importantes. Por un lado tendremos que
implementar un Broadcast Receiver que se encargue de recibir los mensajes, y por otro
lado crearemos un nuevo servicio (concretamente un Intent Service) que se encargue
de procesar dichos mensajes. Esto lo hacemos as porque no es recomendable realizar
tareas complejas dentro del propio broadcast receiver, por lo que normalmente utilizaremos
este patrn en el que delegamos todo el trabajo a un servicio, y el broadcast receiver se
limitar a llamar a ste.
En esta ocasin vamos a utilizar un nuevo tipo especfico de broadcast
receiver,WakefulBroadcastReceiver, que nos asegura que el dispositivo estar despierto
el tiempo que sea necesario para que termine la ejecucin del servicio que lancemos para
procesar los mensajes. Esto es importante, dado que si utilizramos un broadcast receiver
tradicional el dispositivo podra entrar en modo de suspensin (sleep mode) antes de que
terminramos de procesar el mensaje.
Crearemos por tanto una nueva clase que extienda de WakefulBroadcastReceiver, la
llamamosGCMBroadcastReceiver, e implementaremos el evento onReceive() para llamar a
nuestro
servicio
de
procesamiento
de
mensajes,
que
recordemos
lo
llamamos GCMIntentService. La llamada al servicio la realizaremos mediante el
mtodo startWakefulService() que recibir como parmetros el contexto actual, y el mismo
intent recibido sobre el que indicamos el servicio a ejecutar mediante su
mtodosetComponent().
1
public class GCMBroadcastReceiver extends WakefulBroadcastReceiver
2
{
3
@Override
4
public void onReceive(Context context, Intent intent)
5
{
6
ComponentName comp =
7
new ComponentName(context.getPackageName(),
8
GCMIntentService.class.getName());
9
10
startWakefulService(context, (intent.setComponent(comp)));
11
12
setResultCode(Activity.RESULT_OK);
13
}
14 }
428
429
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
S es importante fijarse en que al final del mtodo onHandleIntent(), tras realizar todas las
acciones necesarias para procesar el mensaje recibido, debemos llamar al
mtodo completeWakefulIntent() de nuestro GCMBroadcastReceiver. Esto har que el
dispositivo pueda volver a entrar en modo sleep cuando sea necesario. Olvidar esta llamada
podra implicar consumir rpidamente la batera del dispositivo, y no es lo que queremos,
verdad?
Pues bien, hemos terminado. Ya tenemos nuestro servidor y nuestro cliente GCM
preparados. Si ejecutamos ambas y todo ha ido bien, introducimos un nombre de usuario
en la aplicacin Android, pulsamos Registrar para guardarlo y registrarnos, seguidamente
desde la aplicacin web introducimos el mismo nombre de usuario del cliente y pulsamos el
botn Enviar GCM, en pocos segundos nos debera aparecer la notificacin en la barra de
estado de nuestro emulador como se observa en la imagen siguiente:
430
as, en ocasiones los mensajes tardar varios minutos en recibirse, por lo que tened algo de
paciencia.
Tras esto, accederemos a la seccin API Access para generar el ID de acceso al servicio.
Para ello pulsamos sobre la opcin Create an OAuth 2.0 Client ID, lo que nos llevar a
un asistente de configuracin. En la primera pantalla indicaremos el nombre de la
aplicacin y un logo (opcional).
431
432
433
nombre. En el caso de que alguno de estos pasos no se hayan realizado ya, el intento de
conexin realizado al inicio de la actividad derivar en un error que deber ser tratado
por el usuario. Y esto es precisamente lo que conseguir el usuario al pulsar el botn de
login colocado en la aplicacin. Pero tampoco hay que preocuparse, porque la API de
Google+ proporciona todos los elementos necesarios para que el usuario pueda resolver
estas acciones, por ejemplo el dilogo de seleccin de la cuenta con la que se acceder a
Google+, o el dilogo donde el usuario podr seleccionar los permisos relacionados con
Google+ que desea conceder a la aplicacin (por ejemplo, la visibilidad de determinados
crculos).
Pues bien, veamos cmo plasmamos en el cdigo de la aplicacin todo esto que hemos
contado con palabras. Empezaremos inicializando los componentes necesarios durante la
creacin de la actividad. La conexin con Google+ se sustenta completamente en la
clase PlusClient, por lo que el primer paso ser crear e inicilizar un objeto de este tipo. Esto
lo conseguimos mediante el mtodo PlusClient.Builder(). En esta inicializacin
indicaremos adems las actividades (de acciones, no de Activity) del usuario en la
aplicacin que la propia aplicacin podr publicar en el perfil de Google+ en nombre del
usuario (por ejemplo acciones del estilo a He escuchado tal cancin, He visto tal
imagen o He comentado tal noticia). Existen varios tipos de actividad predefinidas
como BuyActivity, ListenActivity,CommentActivity para
actividades
de
compras,
reproduccin de msica o comentarios (podis revisar la lista completa en esta pgina y un
tipo genrico (AddActivity) para cuando las actividades que enviar nuestra aplicacin a
Google+ con encaja con ninguno de los tipos predefinidos. La lista de actividades que la
aplicacin podr enviar al perfil de Google+ del usuario se configurar mediante el
mtodosetVisibleActivities(), y sern mostradas al usuario al loguearse por primera vez en
la aplicacin de forma que ste sea consciente de ello y pueda conceder su permiso. Ms
tarde pondr una captura de pantalla donde podr verse esto claramente. En nuestro caso
aadiremos por ejemplo las actividadesAddActivity y ListenActivity para ver el efecto.
Adems de esto, para terminar inicializaremos tambin un dilogo de progreso que
utilizaremos ms tarde. Veamos cmo queda el mtodo onCreate()al completo:
1
@Override
2
protected void onCreate(Bundle savedInstanceState)
3
{
4
super.onCreate(savedInstanceState);
5
setContentView(R.layout.activity_main);
6
7
btnSignIn = (SignInButton)findViewById(R.id.sign_in_button);
8
9
plusClient = new PlusClient.Builder(this, this, this)
434
10
11
12
13
14
15
16
17
18
19
.setVisibleActivities(
"http://schemas.google.com/AddActivity",
"http://schemas.google.com/ListenActivity")
.build();
connectionProgressDialog = new ProgressDialog(this);
connectionProgressDialog.setMessage("Conectando...");
//...
}
435
14
15
@Override
16
public void onDisconnected()
17
{
18
Toast.makeText(this, "Desconectado!",
19
Toast.LENGTH_LONG).show();
20
}
21 }
Con lo implementado hasta ahora bastar la mayora de las veces. Sin embargo, como
dijimos al principio, la primera vez que el usuario se intenta conectar se requieren acciones
adicionales, como seleccionar la cuenta a utilizar con Google+ o conceder a la aplicacin
los permisos necesarios para interactuar con nuestro perfil de Google+. En estas
circunstancias la llamada al mtodo connect() no tendr xito. Cmo podemos
solucionarlo? Pues como ya hemos indicado esta situacin intentar solucionarse cuando el
usuario pulse el botn de login de Google+ que hemos colocado en la aplicacin. Pero para
poder solucionarlo antes debemos saber qu ha ocurrido exactamente en la llamada
a connect(). Esto lo podemos saber implementando el evento onConnectionFailed() que se
ejecuta cuando la llamada aconnect() no finaliza correctamente. Este evento recibe como
parmetro un objeto de tipoConnectionResult que contiene el motivo por el que no hemos
podido conectarnos al servicio de Google+. Para poder gestionar este evento haremos que
nuestra actividad implemente otra interfaz ms llamada OnConnectionFailedListener.
1
public class MainActivity extends Activity
2
implements ConnectionCallbacks, OnConnectionFailedListener
3
{
4
//...
5
6
@Override
7
public void onConnectionFailed(ConnectionResult result)
8
{
9
//...
10
11
connectionResult = result;
12
}
13 }
Como podemos ver, en este evento nos limitaremos por el momento a guardar el
objetoConnectionResult para tenerlo disponible cuando el usuario pulse el botn de login.
Y vamos ya por fin con nuestro botn. Qu debemos hacer cuando el usuario pulse el
botn de login? Pues en primer lugar comprobaremos que no estamos ya conectados
mediante una llamada al mtodoisConnected(), en cuyo caso no habr nada que hacer. Si no
estamos an conectados pueden ocurrir dos cosas: que dispongamos ya del resultado del
intento de conexin en forma de objetoConnectionResult, o que an no lo tengamos
436
disponible. Para este ltimo caso utilizaremos el dilogo de progreso que inicializamos en
el onCreate() de la actividad, lo mostraremos mediante su mtodo show()y quedaremos a la
espera de disponer del resultado de la conexin.
En caso de conocer ya el resultado de la conexin, llamaremos a su
mtodostartResolutionForResult(). Este mtodo mgico provocar que se muestren al
usuario las opciones necesarias para resolver los errores detectados durante el intento de
conexin a Google+, entre ellos la seleccin de cuenta o el dilogo de concesin de
permisos de Google+.
1
btnSignIn.setOnClickListener(new OnClickListener() {
2
3
@Override
4
public void onClick(View view)
5
{
6
if (!plusClient.isConnected())
7
{
8
if (connectionResult == null)
9
{
10
connectionProgressDialog.show();
11
}
12
else
13
{
14
try
15
{
16
connectionResult.startResolutionForResult(
17
MainActivity.this,
18
REQUEST_CODE_RESOLVE_ERR);
19
}
20
catch (SendIntentException e)
21
{
22
connectionResult = null;
23
plusClient.connect();
24
}
25
}
26
}
27
}
28 });
Cuando el usuario termine de configurar las opciones de conexin a Google+ se lanzar
automticamente el evento onActivityResult(), momento que aprovecharemos para volver a
realizar la conexin llamando de nuevo a connect() ahora que no deberan quedar acciones
por realizar por parte el usuario. Si todo va bien, este nuevo intento de conexin debera
terminar con xito y el usuario quedara conectado (y entre otras cosas se ejecutar el
evento onConnected() del que ya hemos hablado).
437
1
2
3
4
5
6
7
8
9
10
@Override
protected void onActivityResult(int requestCode, int responseCode, Intent intent)
{
if (requestCode == REQUEST_CODE_RESOLVE_ERR &&
responseCode == RESULT_OK)
{
connectionResult = null;
plusClient.connect();
}
}
Con esto casi hemos terminado. Pero nos faltan un par de detalles por cerrar que antes he
omitido a posta para llevar un orden ms lgico en la explicacin. En primer lugar, qu
ocurre cuando el resultado del primer intento de conexin nos llega despus de que el
usuario haya pulsado el botn de login (y por tanto ya se est mostrando el dilogo de
progreso)? En ese caso no slo guardaremos el resultado de la conexin, sino que
desencadenaremos directamente el proceso de resolucin de errores llamando
astartResolutionForResult() igual que hemos hecho en el evento onClick del botn de login.
De esta forma, el evento onConnectionFailed() quedara finalmente de la siguiente forma:
1
@Override
2
public void onConnectionFailed(ConnectionResult result)
3
{
4
if (connectionProgressDialog.isShowing())
5
{
6
if (result.hasResolution())
7
{
8
try
9
{
10
result.startResolutionForResult(this,
11
REQUEST_CODE_RESOLVE_ERR);
12
}
13
catch (SendIntentException e)
14
{
15
plusClient.connect();
16
}
17
}
18
}
19
20
connectionResult = result;
21 }
Por tlimo, nos queda cerrar el dilogo de progreso una vez que el usuario est
correctamente conectado, lo que haremos en el evento onConnected() y utilizaremos para
ello el mtodo dismiss() del dilogo, quedando finalmente as:
1 @Override
438
2
3
4
5
6
7
8
Al pulsar el botn de Iniciar Sesin debemos ya contar con el resultado de la conexin, por
lo que se nos debe dirigir directamente al proceso de resolucin de errores. En primer lugar
tendremos que seleccionar la cuenta de google con la que queremos contectarnos:
439
En la captura anterior me gustara que prestrais atencin al texto del segundo bloque,
donde indica Permitir que la actividad de aplicaciones y de audio est disponible.
Este mensaje debe coincidir con las actividades que establecimos al llamar al
mtodo setVisibleActivities() al principio del ejemplo. Recuerdo que nosostros
seleccionamos la genrica AddActivity y la de audio ListenActivity.
Tras aceptar esta ltima pantalla deberamos estar ya conectados correctamente a Google+,
apareciendo el mensaje toast que nos informa de ello. Lo que podremos hacer a partir de
aqu queda para el siguiente artculo.
Pero no terminamos an, nos quedan un par de temas importantes por aadir. Si queremos
que nuestra aplicacin cumpla con las polticas de Google+ debemos ofrecer al usuario una
opcin para cerrar sesin en Google+, y otra para que pueda revocar los permisos que ha
concedido a nuestra aplicacin la primera vez que se conect.
En mi ejemplo aadir estas opciones como acciones del men de overflow de la action bar
(si necesitas informacin sobre cmo hacer esto puedes ojear el artculo sobre la action
bar).
440
441
33
34
35
36
37
38
39
40
41
42
Toast.LENGTH_LONG).show();
}
});
}
return true;
default:
return super.onMenuItemSelected(featureId, item);
}
}
Y hasta aqu el primer artculo sobre integracin con Google+. En el siguiente veremos
varias de las funcionalidades que tendremos disponibles al estar conectados con este
servicio.
442
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
443
recuperada. Como ya he dicho, los datos disponibles son muchos y lo mejor es consultar la
documentacin oficial en cada caso.
Si volveis a mirar el cdigo anterior, al final del todo recupero un dato ms, en este caso
utilizando directamente un mtodo del cliente de Google+ en vez de la clase Person. Este
mtodogetAccountName() se utiliza para consultar la direccin de correo electrnico del
usuario logueado, ya que aunque existe un mtodo getEmails() en la clase Person, ste no
devolver la direccin principal del usuario a menos que ste la haya hecho pblica en su
perfil.
Si ejecutamos ahora la aplicacin de ejemplo podris ver los datos del perfil del usuario que
hayis utilizado para hacer login, en mi caso algo as:
Lo siguiente que vamos a recuperar son los contactos incluidos en los crculos del usuario.
Recordad que tal como vimos en el artculo anterior la aplicacin slo tendr acceso a los
crculos a los que usuario haya dado permiso al loguarse en la aplicacin. Otro detalle a
tener en cuenta es que podremos acceder a los contactos pero no a los nombres de los
crculos que los contienen.
Para recuperar los contactos incluidos en los crculos del usuario que son visibles para la
aplicacin utilizaremos el mtodo loadPeople(). Este mtodo nos devolver un
objeto PersonBuffer con todos los contactos de los crculos visibles. Al igual que pasaba
con loadPerson(), esta carga la har de forma asncrona de forma que cuando haya
finalizado se llamar automticamente al mtodoonPeopleLoaded() del objeto que le
pasemos
como
parmetro,
que
debe
implementar
la
interfaz PlusClient.OnPeopleLoadedListener. Para ello haremos lo mismo que antes,
implementaremos dicha interfaz en nustra actividad principal.
1
public class MainActivity extends Activity
2
implements ConnectionCallbacks, OnConnectionFailedListener,
3
PlusClient.OnPersonLoadedListener, PlusClient.OnPeopleLoadedListener
4
{
444
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Override
public void onConnected(Bundle connectionHint)
{
//...
//Personas en mis crculos visibles para la aplicacin:
plusClient.loadPeople(this, Person.Collection.VISIBLE);
}
@Override
public void onPeopleLoaded(ConnectionResult status, PersonBuffer personBuffer, String nextPageTo
{
if (status.getErrorCode() == ConnectionResult.SUCCESS)
{
try
{
int count = personBuffer.getCount();
StringBuffer contactos = new StringBuffer("");
for (int i = 0; i < count; i++)
{
contactos.append(
personBuffer.get(i).getId() + "|" +
personBuffer.get(i).getDisplayName() + "\n");
}
txtContactos.setText(contactos);
}
finally
{
personBuffer.close();
}
}
}
}
445
Por ltimo, comentar que ahora que tenemos los ID de cada contacto, si en algn momento
necesitamos obtener los datos de su perfil podramos utilizar el mismo
mtodo loadPerson() que hemos comentado antes pasndole su ID como segundo
parmetro. As, por ejemplo, si quisiera recuperar los datos del perfil de Roman
Nurik realizaramos la siguente llamada:
1 //Perfil de un contacto:
2 plusClient.loadPerson(this, "113735310430199015092");
Y hasta aqu este segundo artculo de la serie. En el prximo veremos cmo podemos
publicar contenidos en Google+ desde nuestra aplicacin.