Primeros pasos en Android: una app muy sencilla (IV)

Diversión con botones

Ya hemos visto lo mínimo necesario para que una aplicación con interfaz funcione. Apenas hace algo (mostrar una cadena de texto predefinida) pero nos sirve para tener un punto de arranque. Vamos a meterle unas cuantas cosas mas para que tenga algo de “chicha”.

Empecemos por abrir el strings.xml. Vamos a añadir un par de cadenas, una por la interfaz y otra por código, para que tengamos claras todas las formas de hacerlo:

Por interfaz.

Interfaz de strings.xml

Pulsamos en Add . Nos saldrá una ventana para elegir el tipo de value que queremos crear (en este caso una string) y pulsamos OK. Aparecerá un nuevo elemento en el cuadro de la izquierda llamado String y los campos Name y Value (a la derecha) vacíos. En el nombre pondremos miCadena1. No es la forma mas adecuada para dar nombre a elementos, pero por ahora nos vale. ¡Pero en breve hablaremos de las buenas prácticas, no creáis que se me olvida!

En Valor ponemos “Esta cadena no salía antes” tal cual, con las comillas y todo. Guardamos los cambios y entramos al código (pulsando la pestaña inferior strings.xml).

El código de nuestro fichero de cadenas actual es el siguiente:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">¡Hola, Albero Valley!</string>
    <string name="app_name">PrimeraApp</string>
    <string name="miCadena1">&quot;Esta cadena no salía antes&quot;</string>
</resources>

Podemos observar que hay un gran elemento resources que contiene las distintas cadenas como elementos string. ¿Recordáis las comillas? Pues observad que se han convertido en entidades html. Vamos ahora a meter otra cadena, desde el código, a ver qué hace con las comillas.
Bajo la última cadena (miCadena1) introducimos esta línea

   <string name="miCadena2">'Esta OTRA cadena TAMPOCO salía antes'</string>

Al guardar los cambios veremos que aparece una marca de error a la izquierda de nuestra nueva línea. error en strings.xmlNo le han gustado los apóstrofes (‘ ‘ ) y de hecho ya nos avisa en la interfaz gráfica  If you use an apostrophe or a quote in your string, you must either escape it or enclose the whole string in the other kind of enclosing quotes. Si usas apóstrofes o comillas en tu cadena de texto, debes escaparlo (\’) o encerrar toda la cadena en el otro tipo de signo. En este caso, como usamos apóstrofes, vamos a dejar toda la cadena envuelta en comillas dobles:

<string name="miCadena2">"'Esta OTRA cadena TAMPOCO salía antes'"</string>

Al guardar los cambios ahora veremos que desaparece el indicador rojo de error.

Ahora vamos al fichero de layout, nuestro main.xml. En este caso vamos a hacerlo todo desde código, así que entramos por la pestaña inferior main.xml.

Lo primero que vamos a hacer es cambiar la orientación del layout a vertical. Cambiamos el valor del atributo android:orientation de “horizontal” a “vertical” y nos quedaría así

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

Ahora toca identificar los elementos del layout. Usaremos para ello el atributo android:id="@+id/idquequeramosponer". Les daremos los id label1 y label2, deforma que quedará así

<TextView
	android:id="@+id/label1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello"
    />
    <TextView
    android:id="@+id/label2"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/miCadena2"
    />

Si alguna vez diéramos el mismo id a dos elementos diferentes nos saldría una marca de error a la izquierda del elemento infractor.

Añadamos ahora un par de botones, justo debajo del TextView que hemos identificado como label2. Podemos emplear la utilísima función de Eclipse de compleción de código. Escribimos <But y pulsamos las teclas ctrl + espacio (en PC) y nos saldrán las opciones disponibles. Esto vale tanto para los xml como para el código Java.

Definamos nuestros botones: ambos serán tan anchos como la pantalla y sólo tan altos como precisen para mostrar la etiqueta o texto que les definamos. Tanto sus id como el texto de los botones serán boton1 y boton2 respectivamente. En este caso, el texto puede ir dentro del atributo adecuado entrecomillado, no hace falta acudir a las cadenas de strings.xml. El código final sería algo así

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView
	android:id="@+id/label1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello"
    />
    <TextView
    android:id="@+id/label2"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/miCadena2"
    />
    <Button
    android:id="@+id/boton1"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="boton1"
    />
    <Button
    android:id="@+id/boton2"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="boton2"
    />
</LinearLayout>

Con ésto ya estaría listo para nuestro experimento. Toca meterse con el código Java. ¡Por fin!

Iremos a la actividad, Principal.java, y empezaremos por añadir unos cuantos atributos a la clase. Necesitaremos dos atributos TextView y dos atributos Button. Los haremos privados porque no hay necesidad de que sean accesibles desde fuera de esta clase.

   private TextView label1;
   private TextView label2;
   private Button boton1;
   private Button boton2;

Al principio os saldrán marcados como error. Eso se debe a que no hemos importado las clases necesarias. En Eclipse se resuelve con un atajo de teclas (en PC) ctrl + mays + O que se encarga de “arreglar” los imports. Tras esto, toca ir al método onCreate a añadir el código que permita que asociemos estos atributos a los elementos de la ventana. Para ello emplearemos el método findViewById (de ahí que le pusiéramos id a todos los elementos) de la siguiente forma:

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        label1 = (TextView) findViewById(R.id.label1);
        label2 = (TextView) findViewById(R.id.label2);
        boton1 = (Button) findViewById(R.id.boton1);
        boton1 = (Button) findViewById(R.id.boton2);
    }

Podemos observar cómo hacemos referencia a los elementos del layout a través de su id con R.id.nombredelid. Por ello es muy importante que elijamos correctamente los ids, tanto para no repetirnos como para que nos sea mas fácil saber por su id a qué elemento (y de qué ventana!) nos referimos cuando en nuestra app tenemos múltiples elementos de cada tipo en múltiples ventanas.

Ahora toca hacer que los botones reaccionen a los intentos del usuario de activarlos. Hay que darles un Listener para el evento onClick, y en nuestro caso será la propia actividad quién cumpla esa función. El código sería así

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        label1 = (TextView) findViewById(R.id.label1);
        label2 = (TextView) findViewById(R.id.label2);
        boton1 = (Button) findViewById(R.id.boton1);
        boton1 = (Button) findViewById(R.id.boton2);
        boton1.setOnClickListener(this);
        boton2.setOnClickListener(this);
    }

De nuevo nos sale marcado como error. ¿Por qué? Porque la clase Principal NO implementa la interfaz onClickListener. Esto es algo que nos indica el propio Eclipse si dejamos el cursor sobre las líneas con error sugerencias para solucionar un error en Eclipse En este caso nos iremos a la que indica Let ‘Principal’ implement ‘onClickListener’ (podemos dejar fijo este emergente con F2 para así poder desplazarnos por toda la lista) y la pulsaremos. Veremos que el encabezado de la clase cambia:

public class Principal extends Activity implements OnClickListener {

y que ahora aparece marcada como error. Eso se debe a que la Interfaz exige que se implementen los métodos heredados. Nos posicionamos sobre el nombre de la clase (ahora subrayado de rojo), y pulsamos en Add unimplemented methodslo que nos creará automáticamente el método

public void onClick(View v) {
		// TODO Auto-generated method stub

	}

De cabeza al nuevo método. Como tenemos varios elementos en pantalla, lo primero es saber cómo diferenciar a qué elemento se está pulsando. Ello se hace sacando el id de la vista pulsada (el parámetro del método) y comparándolo uno por uno con los diferentes elementos cuyo click queremos controlar.

public void onClick(View v) {
		if (v.getId() == R.id.boton1){
			//pulsado botón1
		}else if(v.getId() == R.id.boton2){
			//pulsado botón2
		}else{
			// nada
		}

	}

Aquí lo que estamos haciendo es ver qué botón se ha pulsado. Cabría la posibilidad de que cualquier otro elemento pulsado activara el método, pero cómo sólo queremos registrar los clicks de los botones, dejamos esa opción sin código alguno.

Ahora vamos a hacer que los TextViews cambien de texto en función de qué botón pulsemos. Ahora haremos los cambios sobre el mismo elemento (label2) para que sea mas vistoso, pero podría ser un botón con cada uno o bien que un botón cambiara ambas… Sky is the limit!

public void onClick(View v) {
		if (v.getId() == R.id.boton1){
			//pulsado botón1
                        label2.setText(R.string.miCadena1);
		}else if(v.getId() == R.id.boton2){
			//pulsado botón2
			label2.setText(R.string.miCadena2);
		}else{
			// nada
		}

	}

Guardamos cambios, ejecutamos el proyecto y ¡a probar los botones!

¡Demonios, da error! Tendremos que mirar en los logs a ver qué ha sido. Iremos a la pestaña de las vistas (las que están agrupadas en la parte inferior de la pantalla si estamos en la perspectiva Java) llamada LogCat. Allí marcaremos el botón circular que tiene una E roja para que sólo visualicemos las trazas de excepción, que es lo que nos indicará dónde está nuestro error.

04-27 12:08:18.611: ERROR/AndroidRuntime(395): FATAL EXCEPTION: main
04-27 12:08:18.611: ERROR/AndroidRuntime(395): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.alberovalley.primeraapp/com.alberovalley.primeraapp.Principal}: java.lang.NullPointerException
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2663)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2679)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.app.ActivityThread.access$2300(ActivityThread.java:125)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2033)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.os.Handler.dispatchMessage(Handler.java:99)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.os.Looper.loop(Looper.java:123)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.app.ActivityThread.main(ActivityThread.java:4627)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at java.lang.reflect.Method.invokeNative(Native Method)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at java.lang.reflect.Method.invoke(Method.java:521)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at dalvik.system.NativeStart.main(Native Method)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): Caused by: java.lang.NullPointerException
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at com.alberovalley.primeraapp.Principal.onCreate(Principal.java:27)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)
04-27 12:08:18.611: ERROR/AndroidRuntime(395): ... 11 more

Si no has visto trazas como esta anteriormente, ver esto será apabullante. Y eso que he eliminado el resto de trazas del emulador. Aconsejo que activéis el AVD, bien por el AVD manager o bien ejecutando el proyecto, que luego en LogCat pulséis el botón que borra las trazas, el de la derecha con la x roja, y volváis a ejecutar el proyecto para que así sólo salgan las trazas de nuestra app.Una vez tengamos sólo las nuestras, todo es cosa de saber dónde buscar. Primero la excepción causada:

04-27 12:08:18.611: ERROR/AndroidRuntime(395): FATAL EXCEPTION: main
04-27 12:08:18.611: ERROR/AndroidRuntime(395): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.alberovalley.primeraapp/com.alberovalley.primeraapp.Principal}: java.lang.NullPointerException

Un nullPointerException. De estos se ven a patadas. En algún punto estamos usando un objeto sin inicializar o pasando un argumento nulo dónde no debemos. Lo siguente a esto es buscar qué lo causó y dónde está.

04-27 12:08:18.611: ERROR/AndroidRuntime(395): Caused by: java.lang.NullPointerException
04-27 12:08:18.611: ERROR/AndroidRuntime(395): at com.alberovalley.primeraapp.Principal.onCreate(Principal.java:27)

Algo normal: un NullPointerException causa un NullPointerException. La cosa está en las siguientes líneas. Hay que buscar las líneas en las que salga identificado nuestro paquete (en este caso com.alberovalley.primeraapp) y ver a dónde nos remite. En nuestro error, la excepción se produce en la línea 27 de Principal.java. Podemos hacer dobleclick sobre esta línea para que nos lleve de inmediato allí.

        boton1 = (Button) findViewById(R.id.boton1);
        boton1 = (Button) findViewById(R.id.boton2);

        boton1.setOnClickListener(this);
        boton2.setOnClickListener(this);

Aquí está el origen de nuestros males: El copy/paste. Si habéis copiado mi código tal cual está, habrá dado el error porque copié la línea de asignación del botón2 sin cambiar la variable a la que lo asignaba. Así, la variable boton1 empezó siendo el boton1 y luego el boton2; mientras que la variable boton2 nunca se asignó, quedando nula. Al pasarle el Listener a un objeto nulo se desató la excepción. Así que el cambio es sencillo.

        boton1 = (Button) findViewById(R.id.boton1);
        boton2 = (Button) findViewById(R.id.boton2);

        boton1.setOnClickListener(this);
        boton2.setOnClickListener(this);

Guardamos los cambios, borramos las trazas de LogCat – para poder localizar mas fácilmente cualquier posible error – y ejecutamos de nuevo. Ahora veremos algo parecido a esto:
Apariencia de la app recién ejecutada

 

 

 

 

 

 

 

Si pulsamos el boton1 lo que veremos será
El texto cambia al pulsar el boton1

 

 

 

 

 

 

Y si pulsamos el boton2…
El texto vuelve a cambiar al pulsar el boton2

 

 

 

 

 

 

Y hasta aquí por ahora. Hemos visto un nuevo elemento de ventana (el botón), hemos aprendido a asociarlo a variables java para poder manejar los elementos desde el código de las actividades y a usar el evento de pulsación onClick. También hemos visto (salvo los que estuvieran pendientes del código y vieran el error con antelación) una excepción en nuestra aplicación, cómo encontrar qué lo provocó y por qué sucedió, que siempre eché en falta explicaciones sobre el por qué y los cómo de los errores en otros tutoriales. Espero que haya gustado.

El código completo de esta app puede encontrarse en el repositorio correspondiente de github

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: