Anatomía de una app básica

Anatomía de una app básica

Después de una aproximación inicial a Android Studio y crear y ejecutar nuestra primera app de Android con Kotlin (Hola Mundo con Android Studio), en esta entrada vamos a repasar algunos de los componentes principales de una aplicación de Android y añadiremos un botón para agregar cierta interactividad a nuestra aplicación.

¡Manos a la obra! Abrimos Android Studio y creamos un nuevo proyecto a partir de una actividad vacía (Empty Activity) que llamaremos TirarDados (recuerda señalar Kotlin como lenguaje y seleccionar los Artefactos AndroidX):

Nuevo proyecto en Android Studio

Ahora nos aparecerán abiertos dos archivos fundamentales: MainActivity.kt y activity_main.xml. El primero es un ejemplo de una actividad. Una Activity es una clase básica de Android con una interfaz de usuario (UI) de la aplicación y que puede recibir eventos de entrada para que los usuarios puedan interactuar y realizar acciones. Las actividades están conformadas por dos partes: la parte lógica del código en el archivo MainActivity y la parte gráfica en el archivo activity_main.xml.

Asegúrate de tener seleccionada la pestaña vertical Proyect en el lado izquierdo de la ventana de Android Studio para ver la estructura de archivos, y entonces comprueba que puedes visualizar tu proyecto de distintas formas seleccionando distintas opciones en el menú desplegable del panel superior, siendo la visualización por defecto Android ya que posiblemente ofrece la forma más fácil de trabajar, pero por ejemplo en alguna ocasión te puede interesar visualizar la jerarquía de archivos del proyecto y entonces puedes seleccionar la vista Project.

Y ya que estás ahí, desde la vista Android vemos dos directorios principales: app y Gradle Scripts. Empieza explorando la carpeta app y las subcarpetas que contiene: manifests, java, generatedJava y res, y empieza a familiarizarte con ellas. Vamos a repasar tres archivos fundamentales de la carpeta app:

app: Archivos principales del proyecto

En app/manifests revisa el archivo AndroidManifest.xml, que podrá ser algo así:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.android.tirardados">

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

Cuando esta aplicación se ejecuta, se inicia la actividad especificada en este archivo, y concretamente en el elemento <activity>, que vemos que es MainActivity. Una aplicación puede contener una o muchas actividades, pero cualquier actividad debe ser declarada en este archivo.

Observa también el elemento <intent-filter> dentro de <activity> y dentro de él los elementos <action> y <category> que le indican a Android dónde iniciar la aplicación cuando el usuario hace clic en el icono del lanzador.

Además, el archivo AndroidManifest.xml también es el lugar donde se definen los permisos que la aplicación necesita como, por ejemplo, la capacidad de la aplicación para leer los contactos, enviar datos a través de Internet o acceder a hardware como la cámara del dispositivo.

La carpeta java contiene el código principal de Kotlin para una aplicación de Android, y dentro de ésta, encontramos tres subdirectorios. La carpeta con el nombre de dominio especificado (com.android.tirardados) contiene todos los archivos del paquete de la app, y en particular el archivo de la clase MainActivity que como vimos es el punto de entrada principal de la aplicación (las otras dos carpetas en la carpeta java se utilizan para el código relacionado con las pruebas).

Por su parte, la carpeta generatedJava contiene archivos que Android Studio genera cuando construye la aplicación, por lo que de momento ahí no vamos a cambiar nada (pero puede ser útil cuando se necesita ver esos archivos durante la depuración).

La carpeta res contiene recursos, que son el contenido estático usado por la aplicación, incluyendo imágenes, texto, diseños de pantalla, estilos y valores, como, por ejemplo, colores hexadecimales o dimensiones estándar utilizados por la app. Esta carpeta tiene sentido en la medida en que Android pretende separar tanto como sea posible el código de este tipo de recursos, lo que facilita mucho la programación y su revisión posterior (imagínate, por ejemplo, que para cambiar el icono de la aplicación únicamente tienes que cambiar el fichero de la imagen manteniendo el nombre anterior pero no tienes que modificar nada en tu código).

Dentro de la carpeta res tenemos el subdirectorio layout (diseño), y dentro de éste el archivo activity_main.xml. Usualmente cualquier actividad se asocia con un archivo de diseño de interfaz de usuario como éste, con extensión xml y ubicado en el directorio res/layout (la parte gráfica de la actividad).

Ahora volvemos a la carpeta principal y desplegamos Gradle Scripts. Sin entrar en detalle, Gradle es un sistema que automatiza la construcción de nuestro código utilizando un lenguaje específico para describir la estructura, la configuración y las dependencias de la aplicación. Por tanto, esta carpeta contiene todos los archivos que necesita el sistema de construcción de la aplicación.

Explorando la carpeta Gradle Scripts encontramos distintos archivos, aunque ahora nos interesan dos: build.gradle(Project: TirarDados) y build.gradle(Module:app).

El primero contiene las opciones de configuración (como los repositorios y dependencias) que son comunes a todos los módulos del proyecto. Además del archivo build.gradle a nivel de proyecto, cada módulo tiene un archivo build.gradle propio. El archivo build.gradle a nivel de módulo permite ajustar las configuraciones de compilación para cada módulo. Este archivo es el que habitualmente se edita para cambiar las configuraciones de construcción a nivel de la aplicación, por ejemplo, para cambia el nivel de SDK que admite la aplicación o cuando se declaran nuevas dependencias.

Así, por ejemplo, si al intentar ejecutar la app te aparece un error como éste:
Error: Default interface methods are only supported starting 
with Android N (--min-api 24): android.view.MenuItem 
androidx.core.internal.view.SupportMenuItem.
setContentDescription(java.lang.CharSequence)
podemos resolverlo escribiendo en el archivo build.gradle del módulo lo siguiente dentro del elemento android (más información en Features of revolutionary release -Java 8 for Android):
android {
...
  compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
...
}

Una vez que hemos examinado los principales directorios y archivos del proyecto, vamos a volver al archivo de la actividad MainActivity:
package com.android.tirardados

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Después del nombre del paquete y las declaraciones de importación, se encuentra la declaración de la clase MainActivity que extiende la clase AppCompatActivity. Ten en cuenta que es muy recomendable usar siempre AppCompatActivity porque así se admiten las funciones modernas de Android y además se ofrece compatibilidad con versiones previas, lo que permite llegar a más dispositivos y usuarios posibles.

Después está el método onCreate que siempre sobrescribe el método de la clase (se trata de un método predefinido que forma parte del "ciclo de vida" de la actividad). onCreate especifica qué diseño (layout) está asociado con la actividad y activa o infla (inflate) ese diseño. Después el método setContentView lo lleva a cabo haciendo referencia al diseño mediante R.layout.activity_main que en realidad es un valor de tipo entero (la clase R se genera al construir la aplicación y utiliza un valor entero para hacer referencia a cada activo (asset) de la aplicación, incluyendo los contenidos del directorio res.

Podríamos conocer ese valor añadiendo un mensaje con la clase Log que podremos ver en la pestaña Logcat del monitor de Android (y allí filtramos los mensajes tipo Info):
// requiere import android.util.Log
Log.i("NUMERO LAYOUT", "${R.layout.activity_main}")

En este caso, R.layout.activity_main hace referencia a un valor entero que identifica el archivo activity_main.xml (sin especificar la extensión) en el subdirectorio layout de la clase R generada. Esto es muy útil porque la clase R nos permitirá hacer referencia a los recursos de la aplicación como imágenes, textos y otros elementos dentro del archivo de diseño.

Todas las actividades de la aplicación tienen un archivo de diseño asociado en el directorio res/layout, que es un archivo XML que expresa cómo se ve realmente esa actividad. Un archivo de diseño hace esto definiendo vistas (views) y definiendo dónde aparecen esas vistas en la pantalla.

Las views son cosas como texto, imágenes y botones que extienden la clase View. Algunos ejemplos de tipos de views son TextView, Button, ImageView y CheckBox.

Ahora vamos a examinar el archivo de diseño activity_main.xml que se encuentra en app/src/main/res/layout. Para trabajar con este archivo Android Studio ofrece dos editores: Design (editor gráfico que previsualiza el resultado del diseño) y Text (editor de texto). Cambiamos de uno a otro con sus respectivas pestañas en la parte inferior de la ventana.

Ahora selecionamos el editor de texto (Text) y eliminamos todo el código para crear un nuevo diseño desde cero. Copia y pega el siguiente código en su lugar:
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".MainActivity">

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"/>

</LinearLayout>

Si al pegarlo se ha desajustado alguna sangría puedes resolverlo con un clic desde Code --> Reformat Code.

Ahora en el código del archivo activity_main.xml el elemento de nivel superior o raíz es <LinearLayout> que es una vista (view) del tipo ViewGroup. Este tipo de views son contenedores de otras views y sirven para especificar sus posiciones en pantalla.

En un archivo de diseño todas las views están organizadas jerárquicamente, con un elemento XML superior como raíz de esa jerarquía, que contiene otras views o grupos de views. En este caso la raíz es un grupo de views de tipo LinearLayout que organiza de forma lineal las views que contiene, una detrás de otra (vertical u horizontalmente).

Cuando iniciamos un nuevo proyecto el elemento superior predeterminado es un ConstraintLayout, que es muy práctico en muchas aplicaciones, pero ahora vamos a utilizar un LinearLayout porque es más simple y ya tendremos ocasión en futuras entradas para profundizar en las características de los distintos tipos de grupos de views.

Dentro de la etiqueta LinearLayout tenemos el atributo android:layout_width con el valor match_parent lo que significa que su anchura se ajusta para que coincida con la misma anchura que su padre (el elemento que lo contiene), pero como este grupo de views es el elemento raíz, el resultado es que el diseño se expande a todo el ancho de pantalla.

Otro atributo que vemos es layout_height con el valor de wrap_content, lo que hace que la altura de LinearLayout coincida con la altura combinada de todas las views que contiene, que vemos que por ahora solo es un TextView. Este TextView tiene definidos los atributos android:layout_width y android:layout_height como wrap_content, por lo que esta view ocupará solo el espacio requerido por su contenido, que viene definido por el atributo android:text cuyo valor es un String (Hello World!) que se muestra en pantalla.

A partir de ahora vamos a empezar escribir código para concretar el aspecto y darle funcionalidad a nuestra aplicación, por lo que si estás aprendiendo es mucho mejor ir escribiendo cada línea en lugar de copiar y pegar porque enseguida notarás que Android Studio proporciona sugerencias muy interesantes de autocompletado.

Primero vamos a añadir un botón a nuestra app. Para ello, después del elemento TextView empezamos a escribir <Button... y Android Studio enseguida nos ofrece varias posibilidades, entre ellas Button, la seleccionamos y entonces presenta por defecto los atributos layout_width y layout_height y sugiere varios valores, y nosotros elegimos wrap_content en ambos para que el botón tenga el mismo ancho y alto que la etiqueta de texto que va a contener. Para agregar esa etiqueta escribimos un atributo android:text (Android Studio también lo facilita mucho sugiriendo varias posibilidades según vamos escribiendo) con el valor del String "Dado", quedando de esta manera:
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Dado"/>

Observamos que el editor resalta en amarillo el atributo de texto del botón, lo que indica una sugerencia o una advertencia. En este caso, se debe a que el texto "Dado" está codificado en la etiqueta del botón, pero debería ser parte de un recurso (como dijimos, se pretende separar el código de cualquier otra cosa). Por eso se considera una buena práctica colocar las cadenas de texto en un archivo separado, en un archivo llamado strings.xml ubicado en el directorio res/values (ábrelo y échale un vistazo).

Tener los textos en un archivo separado facilita su administración, especialmente si se usan más de una vez, pero además es fundamental para ofrecer traducciones de la aplicación ya que se necesita crear un archivo de recursos de texto para cada idioma.

Android Studio facilita esto, y para ello nos situamos sobre la palabra Dados y pulsamos sobre la bombilla de ayuda que aparace (o pulsamos Alt+Intro) y del menú emergente seleccionamos la opción Extract string resource. En la ventana que se abre podemos dejar dado como Resource name y Dado como Resource value, y aceptamos con OK. Ahora vemos que el texto del elemento Button se ha sustituido por @string/dado, que es una referencia al valor en ese recurso, y si volvemos al archivo /res/values/strings.xml comprobamos que existe un nuevo recurso llamado dado con el valor Dado:
<resources>
    <string name="app_name">TirarDados</string>
    <string name="dado">Dado</string>
</resources>

Por cierto, otro recurso de texto que vemos en este archivo es el nombre de la aplicación, que suele aparecer en la barra de la aplicación en la parte superior de la pantalla (y lo podemos cambiar desde aquí).

Podemos volver al archivo activity_main.xml para ver cómo va quedando el diseño que ahora cuenta con dos elementos: un TextView y un Button. Para verlo tenemos dos opciones: cambiar al modo de diseño pulsando en la pestaña Design o bien desde el mismo editor de texto pulsar sobre la pestaña preview que hay a la derecha de la ventana.

Vemos que ambos elementos están alineados horizontalmente en la parte superior izquierda de la pantalla. El grupo de views LinearLayout coloca los elementos que contiene uno tras otro, ya sea en una fila (horizontal es el valor por defecto) o verticalmente en una pila. Para cambiar esta distribución podemos agregar a la etiqueta superior LinearLayout un atributo de orientación con el valor "vertical": android:orientation="vertical", lo que coloca el botón debajo del texto.

Siguiendo con el diseño, estaría bien que ambos elementos (texto y botón) quedaran bien alineados, y para ello podemos agregar a ambos el atributo android:layout_gravity con el valor "center_horizontal", lo que consigue alinear ambos elementos a lo largo del centro del eje horizontal. También sería buena idea separar ambos elementos de la parte superior, para lo cual agregamos otro atributo android:layout_gravity a LinearLayout pero esta vez con el valor "center_vertical", y así, al utilizarlo en el elemento padre, se centran todos los elementos secundarios a la vez.

Otra pequeña mejora en el estilo de nuestra app sería aumentar el tamaño del texto en el TextView, que lo podemos hacer con el atributo android:textSize y un valor de "30sp" (sp significa píxeles escalables, que es una medida para ajustar el tamaño de texto independientemente de la calidad de visualización del dispositivo).

Al compilar y ejecutar nuestra app se ve de esta manera:

Anatomía de una app básica

Ahora vamos a darle alguna funcionalidad a ese botón, y para eso tenemos que poder utilizar el botón desde la actividad (MainActivity) que es responsable de definir la interacción de los elementos de la aplicación. Para poder referirnos al botón desde la actividad necesitamos primero asignarle una identificación única e inequívoca en el archivo de diseño, y esto lo hacemos con el atributo android:id y un valor, por ejemplo: android:id="@+id/dado_boton".

Al crear un ID para una view en el archivo de diseño, Android Studio crea una constante entera en la clase R generada, y el prefijo @+id en el nombre del botón le dice al compilador que agregue ese valor a la clase R, por lo que todas las ID de las views deben contar con ese prefijo.

Ahora ya podemos hacer referencia al botón desde al archivo MainActivity, lo que se puede hacer de varias maneras. Una de ellas, posiblemente la más usada hasta hace poco tiempo aunque tiende al desuso, es el método findViewById con la referencia del elemento:
// requiere import android.widget.Button
val dadoBoton: Button = findViewById(R.id.dado_boton)

Aunque lo anterior funciona, seguramente es mucho más elegante y práctico hacer uso de las extensiones de Android de esta manera:
// requiere import kotlinx.android.synthetic.main.activity_main.*
val dadoBoton: Button = dado_boton

Y lo mismo lo podemos escribir eliminando la especificación explícita de tipo:
val dadoBoton = dado_boton

Ahora ya podemos agregarle un controlador o manejeador (handler) para que al pulsarlo suceda algo, en este caso, mostrar un aviso o toast, que es un mensaje que aparece en la pantalla durante un breve periodo de tiempo.

Un controlador de clic (click handler) es un método que se invoca cada vez que el usuario hace clic sobre un elemento, en este caso un botón. Para crear este controlador necesitamos un método que realice alguna operación (mostrar el aviso) y otro método (setOnClickHandler) que conecte el botón con el método del controlador.

Para ello, en el archivo de la clase MainActivity, después de onCreate, creamos una función privada llamada tirarDado:
private fun tirarDado() {

}

Y en su interior creamos un toast con el método Toast.makeText() que requiere:
  1. Un objeto de contexto que le permite comunicarse y obtener información sobre el estado actual del sistema operativo Android (dado que AppCompatActivity es una subclase de contexto, podemos usar la palabra reservada this como contexto).
  2. El mensaje a mostrar, por ejemplo "Botón pulsado".
  3. El tiempo durante el que se muestra el mensaje (LENGTH_SHORT o LENGTH_LONG).
  4. Finalmente el método show() muestra el aviso.
private fun tirarDado() {
    Toast.makeText(this, "Botón pulsado",
        Toast.LENGTH_SHORT).show()
}

Ahora dentro de onCreate, después de llamar al botón, podemos asignar la función tirarDado() al objeto dadoBoton como un controlador de clic:
val dadoBoton = dado_boton
dadoBoton.setOnClickListener { tirarDado() }

Entonces la clase MainActivity se ve así:
package com.android.tirardados

// import android.widget.Button
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //val dadoBoton: Button = findViewById(R.id.dado_boton)
        val dadoBoton = dado_boton
        dadoBoton.setOnClickListener { tirarDado() }
    }

    private fun tirarDado() {
        Toast.makeText(
            this, "Botón pulsado",
            Toast.LENGTH_SHORT
        ).show()
    }
}

Y al ejecutar la aplicación y pulsar el botón nos debe mostrar el mensaje de aviso:

Anatomía de una app básica

Ahora vamos a modificar el método tirarDado() para que realice otras tareas, concretamente cambiar el texto del TextView y mostrar un número aleatorio entre 1 y 6.

Para ello vamos a utilizar los conceptos vistos hasta ahora. Primero abrimos activity_main.xml y añadimos un ID al TextView: android:id="@+id/resultado". Después abrimos MainActivity y en el método tirarDado(), comentamos la línea para mostrar el Toast y nos referimos al TextView utilizando su ID, y lo asignamos a una variable:
val resultadoText = resultado

Y le asignamos un nuevo valor a la propiedad resultado.text para cambiar el texto mostrado:
resultadoText.text = "Dado lanzado!"

Ejecutamos para comprobar que funciona (si ya tenemos el emulador con nuestra app en marcha, en lugar que pulsar sobre el icono verde 'Run app (Mayús+F10)' podemos simplemente pulsar sobre el icono en forma de rayo amarillo que está a su lado 'Apply Changes (Ctrl+F10)'.

Para mostrar un número aleatorio que simule el lanzamiento del dado, cada vez que se pulse el botón el programa debe eligir un número entre 1 y 6 y actualizar el texto del TextView para mostrarlo.

En activity_main.xml cambiamos el valor de android:text a "Lanza el dado!" (o mejor, creamos un recurso en el archivo strings.xml), después en el método tirarDado obtenemos el número aleatorio y lo mostramos en el TextView:
val randomInt = Random().nextInt(6) + 1 // requiere import java.util.Random
val resultadoText = resultado
//resultadoText.text = "Dado lanzado!"
resultadoText.text = randomInt.toString()

Aplicamos los cambios y ahora cada vez que pulsamos el botón nos muestra un número distinto (salvo que salga el mismo) entre 1 y 6.



A continuación se muestran cómo han quedado finalmente los archivos MainActivity.kt, activity_main.xml y strings.xml:
// MainActivity.kt
package com.android.tirardados

// import android.widget.Button
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.util.Random

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //val dadoBoton: Button = findViewById(R.id.dado_boton)
        val dadoBoton = dado_boton
        dadoBoton.setOnClickListener { tirarDado() }
    }

    private fun tirarDado() {
        val randomInt = Random().nextInt(6) + 1
        /*Toast.makeText(
            this, "Botón pulsado",
            Toast.LENGTH_SHORT
        ).show()*/
        val resultadoText = resultado
        //resultadoText.text = "Dado lanzado!"
        resultadoText.text = randomInt.toString()
    }
}
<!-------activity_main.xml----->
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_gravity="center_vertical"
        tools:context=".MainActivity">

    <TextView
            android:id="@+id/resultado"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:textSize="30sp"
            android:text="@string/lanza_el_dado"/>

    <Button
            android:id="@+id/dado_boton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/dado"/>

</LinearLayout>
<!-------strings.xml----->
<resources>
    <string name="app_name">TirarDados</string>
    <string name="dado">Dado</string>
    <string name="lanza_el_dado">Lanza el dado!</string>
</resources>

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos