Herramientas de usuario

Herramientas del sitio


bloque1:moviles

Programación de dispositivos móviles con Android

Android

Android es un sistema operativo basado en Linux, diseñado principalmente para dispositivos móviles como teléfonos y tabletas, aunque también se utiliza en relojes, televisores, automóviles y otros dispositivos.

Para desarrollar aplicaciones Android se utiliza el Android SDK (Software Development Kit), un conjunto de herramientas, bibliotecas y APIs que permiten crear, compilar, probar y depurar aplicaciones.

De forma simplificada podemos distinguir:

  • Android: el sistema operativo que se ejecuta en el dispositivo
  • Android SDK: las herramientas y APIs utilizadas para desarrollar aplicaciones
  • Android Studio: el entorno de desarrollo utilizado para escribir y gestionar nuestros proyectos

Las aplicaciones Android se ejecutan sobre ART (Android Runtime), el entorno de ejecución utilizado actualmente por Android.

Android Studio

Android Studio es el entorno de desarrollo integrado (IDE) oficial para crear aplicaciones Android. Está basado en IntelliJ IDEA e incorpora las herramientas necesarias para facilitar el desarrollo.

Entre sus principales características se encuentran:

  • Editor de código para Kotlin y Java
  • Integración con Gradle para compilar los proyectos
  • Gestión del Android SDK
  • Emulador de dispositivos Android
  • Herramientas de depuración
  • Diseño y previsualización de interfaces
  • Integración con sistemas de control de versiones como Git

Durante el módulo utilizaremos Android Studio, Kotlin y el Android SDK para desarrollar nuestras aplicaciones.

Android Studio es el IDE, pero por sí solo no contiene todas las APIs necesarias para desarrollar aplicaciones. Para ello necesita utilizar el Android SDK.

Android SDK

El Android SDK (Software Development Kit) es el conjunto de herramientas, bibliotecas y APIs necesarias para desarrollar aplicaciones para Android.

El SDK incluye, entre otros elementos:

  • Las APIs correspondientes a las diferentes versiones de Android
  • Herramientas de compilación
  • Herramientas para comunicarse con dispositivos Android
  • Herramientas de depuración
  • Gestión de dispositivos virtuales
  • Utilidades utilizadas internamente por Android Studio

Android Studio permite instalar y gestionar estos componentes mediante el SDK Manager. Por ejemplo, un equipo puede tener instaladas diferentes versiones del SDK:

Android SDK
|
+-- Android API 35
+-- Android API 36
+-- Android API 37
|
+-- Build Tools
|
+-- Platform Tools
    |
    +-- adb

No es necesario instalar todas las versiones disponibles. Normalmente se instalan únicamente aquellas que van a utilizarse para desarrollar y probar las aplicaciones.

Versiones de Android y niveles de API

Android ha tenido numerosas versiones desde su aparición. Cada versión incorpora nuevas funcionalidades y modificaciones en el sistema operativo. Para identificar las capacidades disponibles en cada versión, Android utiliza un número denominado API Level.

Por ejemplo:

Versión de Android API Level
Android 14 34
Android 15 35
Android 16 36
Android 17 37

El nivel de API resulta especialmente importante durante el desarrollo, ya que determina qué funcionalidades de Android podemos utilizar.

En los proyectos encontraremos principalmente tres valores dentro del fichero app/build.gradle.kts

minSdk = 24
targetSdk = 36
compileSdk = 37
Propiedad Significado
minSdk Versión mínima de Android en la que puede instalarse la aplicación
targetSdk Versión de Android para la que se ha preparado y probado principalmente la aplicación
compileSdk Versión de las APIs que utiliza el compilador para construir la aplicación

Por ejemplo:

minSdk = 24
targetSdk = 36
compileSdk = 37

significa que la aplicación:

  • puede instalarse desde API 24
  • está preparada principalmente para el comportamiento de API 36
  • puede utilizar las APIs disponibles durante la compilación con API 37

Que una aplicación utilice compileSdk = 37 no significa que solamente pueda ejecutarse en dispositivos con API 37.

La versión mínima en la que puede instalarse viene determinada por minSdk.

Estructura básica de un proyecto Android

Cuando creamos un proyecto nuevo en Android Studio se genera automáticamente una estructura de directorios y archivos. Una versión simplificada podría ser:

MiAplicacion
|
+-- app
|   |
|   +-- manifests
|   |   |
|   |   +-- AndroidManifest.xml
|   |
|   +-- kotlin
|   |   |
|   |   +-- MainActivity.kt
|   |
|   +-- res
|       |       
|       +-- drawable
|       +-- layout
|       +-- mipmap
|       +-- values
|
+-- Gradle Scripts

Algunos de sus elementos más importantes son:

Elemento Función
MainActivity.kt Contiene inicialmente el código de la actividad principal
AndroidManifest.xml Define características y componentes importantes de la aplicación
res Contiene recursos como textos, imágenes, iconos o colores
build.gradle.kts Configuración de compilación del proyecto
libs.versions.toml Puede utilizarse para gestionar versiones de librerías y plugins

Vista Android en el IDE

Gradle

Los proyectos Android utilizan Gradle como sistema de compilación. Gradle se encarga, entre otras tareas, de:

  • Compilar el código
  • Gestionar las dependencias
  • Procesar los recursos de la aplicación
  • Ejecutar pruebas
  • Generar los archivos instalables de la aplicación

Por ejemplo, si nuestra aplicación necesita utilizar una determinada librería, Gradle puede descargarla automáticamente. En proyectos actuales es habitual encontrar dependencias definidas mediante un catálogo de versiones:

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)

Las versiones correspondientes pueden encontrarse en:

gradle/libs.versions.toml

Ejecución de una aplicación

Una aplicación Android necesita ejecutarse sobre un dispositivo Android. Tenemos principalmente dos posibilidades:

  • Emulador
  • Dispositivo físico

Ambas serán configuradas desde el Device Manager de Android Studio.

El emulador es capaz de simular diferentes dispositivos Android. Permite seleccionar características como:

  • Tamaño de pantalla
  • Resolución
  • Versión de Android
  • Cantidad de memoria
  • Tipo de dispositivo

Su principal ventaja es que permite probar rápidamente una aplicación utilizando diferentes configuraciones. Sin embargo, el emulador puede consumir una cantidad considerable de memoria y recursos del ordenador.

Por otro lado, si tenemos un dispositivo físico Android también podemos ejecutar directamente la aplicación.

Para hacerlo será necesario activar las Opciones de desarrollador y la Depuración USB o Depuración Wifi en nuestro dispositivo. Después podremos conectar el dispositivo al ordenador ejecutar directamente la aplicación desde Android Studio.

En equipos con recursos limitados puede ser recomendable utilizar dispositivos Android físicos en lugar del emulador.

Fundamentos de una aplicación Android

Una aplicación Android está formada por diferentes componentes que trabajan conjuntamente. Algunos se encargan de mostrar la interfaz, otros de gestionar datos, ejecutar tareas en segundo plano o comunicarse con otras aplicaciones.

En las primeras aplicaciones nos centraremos principalmente en las Activities, los recursos y el archivo AndroidManifest.xml.

Activity

Una Activity representa normalmente una pantalla de una aplicación Android. Por ejemplo, una aplicación podría tener:

Pantalla de inicio     -> MainActivity
Pantalla de ajustes    -> SettingsActivity
Pantalla de detalle    -> DetailActivity

En un proyecto básico creado con Android Studio suele existir inicialmente una clase:

MainActivity.kt

En un proyecto con Jetpack Compose podemos encontrar algo similar a:

class MainActivity : ComponentActivity() {
 
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
 
    // JetPackCompose
    setContent {
        MiAplicacion()
    }
 
    //XML Views
    setContentView(R.layout.activity_main)
}
 
}

El método onCreate() se ejecuta cuando se crea la Activity. Mediante setContent indicamos la interfaz que se mostrará.

Ciclo de vida de una Activity

Una Activity pasa por diferentes estados durante su ejecución: https:// developer.android.com

Android dispone de diferentes métodos asociados a estos cambios:

Método Momento aproximado
onCreate() Cuando se crea la Activity
onStart() Cuando comienza a ser visible
onResume() Cuando el usuario puede interactuar con ella
onPause() Cuando pierde el foco
onStop() Cuando deja de ser visible
onDestroy() Antes de ser destruida

No siempre será necesario utilizar todos estos métodos, pero es importante comprender que Android controla el ciclo de vida de las pantallas.

AndroidManifest.xml

El archivo AndroidManifest.xml contiene información importante sobre la aplicación. Entre otras cosas puede indicar:

  • Las Activities disponibles
  • La Activity que se inicia al abrir la aplicación
  • Los permisos necesarios
  • Características generales de la aplicación

Por ejemplo:

<activity
    android:name=".MainActivity"
    android:exported="true">
</activity>

En el AndroidManifest.xml, se declara cuál es la actividad principal mediante un filtro de intención (<intent-filter>) con la acción MAIN y la categoría LAUNCHER.

<intent-filter>
   <action android:name="android.intent.action.MAIN" />
   <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

Recursos

Android permite separar determinados elementos del código fuente mediante el sistema de recursos. Estos recursos se almacenan dentro del directorio app/src/main/res (res → resources)

Algunos directorios habituales son:

Directorio Contenido
drawable Imágenes y recursos gráficos
mipmap Iconos de la aplicación
values Textos, colores, temas y otros valores
layout ficheros XML Views en casa de app basadas en ellas

Por ejemplo, los textos pueden almacenarse en:

res/values/strings.xml
<string name="app_name">Mi aplicación</string>
<string name="saludo">Hola Android</string>

Separar los textos del código facilita, entre otras cosas, la traducción de las aplicaciones a distintos idiomas.

Context

En Android, Context es un objeto que permite acceder a recursos y servicios de la aplicación y del sistema. Por ejemplo, para mostrar un mensaje, leer un texto de los recursos o abrir otra pantalla, Android necesita saber desde qué entorno se realiza la operación. Ese entorno se proporciona mediante un Context.

Por ejemplo, puede utilizarse para:

  • Acceder a recursos
  • Iniciar otras Activities
  • Acceder a servicios del sistema
  • Mostrar determinados mensajes
Operación Ejemplo
Acceder a textos de recursos context.getString(R.string.app_name)
Mostrar un mensaje breve Toast.makeText(context, …)
Acceder a archivos internos context.filesDir
Obtener servicios del sistema Acceder al servicio de notificaciones, por ejemplo.
Iniciar otra actividad context.startActivity(intent)

El contexto se suele dividir en dos tipos:

  1. Activity Context (Contexto de Actividad):
    • Está ligado al ciclo de vida de una pantalla o Activity específica.
    • Se utiliza para operaciones visuales, mostrar cuadros de diálogo (dialogs), inflar vistas o iniciar otra actividad.
    • Se suele referenciar mediante this o MyActivity.this.
  2. Application Context (Contexto de Aplicación):
    • Está ligado al ciclo de vida de toda la aplicación, desde que se abre hasta que se cierra.
    • Sobrevive a los cambios de configuración (como girar la pantalla).
    • Se obtiene llamando a getApplicationContext().
    • Se debe usar para operaciones globales que no dependen de una pantalla visual específica (por ejemplo, inicializar librerías, bases de datos o gestores de red.

Depuración y Logcat

Android Studio incluye Logcat, una herramienta que permite visualizar mensajes generados por la aplicación y por el sistema Android. Podemos escribir nuestros propios mensajes utilizando:

Log.d("MI_APP", "Aplicación iniciada")

Esto resulta muy útil para comprobar qué está ocurriendo durante la ejecución y localizar errores.

import android.util.Log
. . .
Log.d("Alumnos", "Alumno guardado")      // Depuración
Log.i("Alumnos", "Aplicación iniciada")  // Información
Log.w("Alumnos", "Nombre vacío")        // Advertencia
Log.e("Alumnos", "Error al guardar")    // Error

En Android no debemos depender de println() como hacíamos en las aplicaciones de consola. Debemos utilizar Logcat.

Desarrollo de interfaces

Existen dos grandes formas de construir interfaces en Android:

  • Views y XML, sistema tradicional de interfaces Android
  • Jetpack Compose, sistema moderno de interfaces declarativas

En Android Studio tenemos diferentes plantillas para crear un proyecto:

  • No Activity: Crea la estructura del proyecto sin una pantalla inicial. Tú añades las actividades y la interfaz. Útil si quieres configurarlo todo manualmente.
  • Empty Activity: Crea una pantalla mínima con Jetpack Compose. Es la opción para empezar una interfaz escrita en Kotlin con funciones @Composable.
  • Gemini API Starter: Crea una base para integrar IA de Gemini dentro de la aplicación.
  • Navigation UI Activity: Crea una aplicación con Compose y una estructura de navegación preparada para varias pantallas o secciones.
  • Basic Views Activity: Crea una base con Views y XML, con componentes como barra superior, botón flotante y navegación entre pantallas mediante fragments.
  • Empty Views Activity: Crea una actividad sencilla con diseño XML. Es la opción más clara para empezar con botones, campos de texto y eventos.
  • Responsive Views Activity: Crea una interfaz con Views y XML preparada para adaptar su distribución y navegación a distintos tamaños de pantalla.
  • Game Activity (C++): Prepara una base para desarrollar juegos con C/C++, con soporte para gestionar aspectos como la entrada del usuario y el ciclo de vida.
  • Native C++: Prepara un proyecto Android con código C/C++. Útil para bibliotecas nativas, procesamiento intensivo o reutilización de código C++.
  • Kotlin Multiplatform: Prepara un proyecto para compartir código Kotlin entre plataformas, por ejemplo Android e iOS.

Vamos a simplificar el proceso, y en nuestro caso usaremos:

  • Empty Activity: Para construir una app usando el kit JetPack Compose
  • Empty Views Activity: Para construir una app cuando queramos usar el sistema de vistas XML

Funciones Composable

Las funciones que forman parte del diseño de la interfaz se marcan con la anotación:

@Composable

Por ejemplo:

@Composable
fun Saludo() {
    Text("Hola Android")
}

Una función @Composable puede llamar a otras funciones @Composable para construir interfaces más complejas.

Por ejemplo:

@Composable
fun PantallaPrincipal() {
    Column {
        Text("Bienvenido")
        Text("Mi primera aplicación")
    }
}

Mostrar la interfaz

Desde la Activity podemos indicar qué interfaz queremos mostrar utilizando:

setContent {
    PantallaPrincipal()
}

Un ejemplo completo sería:

class MainActivity : ComponentActivity() {
 
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
 
    setContent {
        PantallaPrincipal()
    }
}
 
}

XML Views

XML Views es el sistema tradicional de desarrollo de interfaces. En este sistema se separa la definición de la interfaz (XML) del código Kotlin que controla su funcionamiento:

  • XML: define los componentes visuales, su posición y apariencia
  • Kotlin: gestiona los eventos, modifica los componentes y ejecuta la lógica de la aplicación

Los archivos XML que definen las interfaces se encuentran normalmente en: app/src/main/res/layout/

Por ejemplo, el archivo activity_main.xml puede contener:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Introduce tu nombre" />
 
    <EditText
        android:id="@+id/etNombre"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Nombre" />
 
    <Button
        android:id="@+id/btnGuardar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Guardar" />
 
</LinearLayout>

Cada etiqueta XML representa un componente visual o un contenedor.

Componentes básicos (Views)

Las Views son los elementos que forman la interfaz gráfica.

Componente Función
TextView Muestra texto
EditText Permite introducir texto
Button Botón que puede ejecutar acciones
ImageView Muestra imágenes
CheckBox Casilla de selección
RadioButton Permite seleccionar una opción
Switch Selector activado/desactivado
ProgressBar Indicador de progreso

Contenedores (Layouts)

Los Layouts son componentes que permiten organizar y posicionar otros elementos de la interfaz.

Layout Función
LinearLayout Organiza componentes vertical u horizontalmente
ConstraintLayout Posiciona componentes mediante restricciones entre ellos
FrameLayout Permite colocar componentes superpuestos
ScrollView Permite desplazar verticalmente un contenido que no cabe en pantalla

Atributos XML

Cada componente dispone de atributos que determinan su apariencia y comportamiento. Algunos de los más importantes son:

Atributo Función
android:id Identificador del componente
android:layout_width Anchura
android:layout_height Altura
android:text Texto mostrado
android:hint Texto de ayuda en campos de entrada
android:orientation Orientación de un LinearLayout
android:padding Espaciado interior
android:layout_margin Espaciado exterior
android:visibility Visibilidad del componente

Para definir tamaños se utilizan habitualmente:

  • match_parent: ocupa el espacio disponible del contenedor
  • wrap_content: se ajusta al contenido
  • dp: unidad de medida independiente de la densidad de pantalla
  • sp: unidad utilizada principalmente para tamaños de texto

Relación entre XML y Activity

Una Activity puede cargar una interfaz definida en XML mediante el método setContentView(). Por ejemplo, en MainActivity.kt:

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

La expresión R.layout.activity_main hace referencia al archivo res/layout/activity_main.xml.

Android genera automáticamente la clase R, que permite acceder a los recursos de la aplicación desde Kotlin.

Manejo de eventos y componentes

Acceso a los componentes XML

Para acceder desde Kotlin a las vistas definidas en un archivo XML tenemos principalmente dos alternativas:

  • findViewById(): busca una vista mediante su identificador XML
  • ViewBinding: genera una clase que permite acceder directamente a las vistas del layout

1. Utilizando findViewById() Es la forma más sencilla de comprender cómo se relacionan los componentes XML con el código Kotlin. Supongamos que en nuestro XML tenemos:

<EditText
    android:id="@+id/etNombre"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Nombre" />
 
<Button
    android:id="@+id/btnGuardar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Guardar" />

Desde nuestra Activity podemos obtener referencias a esas vistas:

class MainActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        setContentView(R.layout.activity_main)
 
        val etNombre = findViewById<EditText>(R.id.etNombre)
        val btnGuardar = findViewById<Button>(R.id.btnGuardar)
    }
}

La función findViewById<T>() recibe el identificador de la vista y devuelve una referencia al componente del tipo indicado.

Debo ejecutar previamente a setContentView() para cargar el layout.

2. Utilizando ViewBinding ViewBinding es una alternativa más moderna que evita tener que buscar manualmente cada componente. Para activarlo añadimos en app/build.gradle.kts:

android {
    buildFeatures {
        viewBinding = true
    }
}

Después de sincronizar Gradle, Android genera una clase de binding para cada layout XML. Por ejemplo, para activity_main.xml genera ActivityMainBinding.

class MainActivity : AppCompatActivity() {
 
    private lateinit var binding: ActivityMainBinding
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
 
        val nombre = binding.etNombre.text.toString()
    }
}

ViewBinding reduce el código repetitivo y permite acceder a las vistas con comprobación de tipos.

Lectura y modificación de componentes

Una vez obtenida una referencia a una vista, podemos consultar o modificar sus propiedades. Por ejemplo, suponiendo que tenemos los componentes etNombre, tvResultado y btnGuardar:

val etNombre = findViewById<EditText>(R.id.etNombre)
val tvResultado = findViewById<TextView>(R.id.tvResultado)
val btnGuardar = findViewById<Button>(R.id.btnGuardar)

Podemos leer el contenido de un EditText:

val nombre = etNombre.text.toString()

Modificar el contenido de un TextView:

tvResultado.text = "Hola $nombre"

Gestión de eventos

Un evento es una acción que ocurre durante la ejecución de la aplicación, como pulsar un botón, escribir texto o seleccionar una opción.

Para responder a estos eventos utilizamos listeners, que permiten ejecutar código cuando sucede una determinada acción.

Por ejemplo, podemos detectar la pulsación de un botón mediante setOnClickListener():

val btnGuardar = findViewById<Button>(R.id.btnGuardar)
val etNombre = findViewById<EditText>(R.id.etNombre)
val tvResultado = findViewById<TextView>(R.id.tvResultado)
 
btnGuardar.setOnClickListener {
    val nombre = etNombre.text.toString()
    tvResultado.text = "Hola $nombre"
}

El código incluido dentro de la lambda se ejecuta cada vez que el usuario pulsa el botón.

Eventos más habituales

Listener o método Evento Componentes habituales
setOnClickListener() Pulsación sobre un elemento Button, ImageButton, TextView, ImageView
setOnLongClickListener() Pulsación prolongada Button, ImageView, cualquier View
addTextChangedListener() Modificación del texto EditText
setOnCheckedChangeListener() Cambio de selección o activación CheckBox, Switch, RadioButton
setOnFocusChangeListener() Una vista recibe o pierde el foco EditText y otras Views

Algunos componentes, como RecyclerView, gestionan habitualmente las pulsaciones de sus elementos mediante listeners definidos en las vistas de cada elemento.

Ejemplo: detectar cambios en un EditText

Podemos utilizar doOnTextChanged() para ejecutar código cada vez que se modifica el contenido de un campo.

etNombre.doOnTextChanged { texto, _, _, _ ->
    tvResultado.text = texto.toString()
}

Para utilizarlo necesitamos importar:

import androidx.core.widget.doOnTextChanged

Ejemplo: detectar cambios en un CheckBox

val checkBox = findViewById<CheckBox>(R.id.checkBox)
 
checkBox.setOnCheckedChangeListener { _, marcado ->
    if (marcado) {
        Log.d("MI_APP", "Casilla seleccionada")
    }
}

Validación de datos

Antes de procesar los datos introducidos por el usuario es recomendable comprobar que son correctos. Por ejemplo, podemos evitar que se guarde un nombre vacío:

val nombre = etNombre.text.toString().trim()
 
if (nombre.isEmpty()) {
    etNombre.error = "Introduce un nombre"
} else {
    tvResultado.text = "Hola $nombre"
}

Para campos numéricos podemos utilizar toIntOrNull():

val etEdad = findViewById<EditText>(R.id.etEdad)
val edad = etEdad.text.toString().toIntOrNull()
 
if (edad == null || edad <= 0) {
    etEdad.error = "Edad no válida"
}

De esta forma evitamos errores al intentar convertir cadenas de texto que no contienen números válidos.

Mostrar mensajes al usuario

Android proporciona diferentes mecanismos para comunicar información al usuario. Uno de los más sencillos es Toast, que muestra un pequeño mensaje temporal:

Toast.makeText(this, "Alumno guardado", Toast.LENGTH_SHORT).show()

El primer parámetro es un Context, el segundo es el mensaje y el tercero indica la duración. También podemos utilizar Snackbar para mostrar mensajes asociados a una vista, incluyendo opcionalmente una acción.

Ejemplo completo

Supongamos que tenemos una interfaz XML con los siguientes componentes:

  • Un EditText con id etNombre
  • Un EditText con id etEdad
  • Un Button con id btnGuardar
  • Un TextView con id tvResultado

Podemos implementar la siguiente Activity:

class MainActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        setContentView(R.layout.activity_main)
 
        // Referencias a los componentes XML
        val etNombre = findViewById<EditText>(R.id.etNombre)
        val etEdad = findViewById<EditText>(R.id.etEdad)
        val btnGuardar = findViewById<Button>(R.id.btnGuardar)
        val tvResultado = findViewById<TextView>(R.id.tvResultado)
 
        // Evento del botón
        btnGuardar.setOnClickListener {
 
            val nombre = etNombre.text.toString().trim()
            val edad = etEdad.text.toString().toIntOrNull()
 
            if (nombre.isEmpty()) {
                etNombre.error = "Introduce un nombre"
            } else if (edad == null || edad <= 0) {
                etEdad.error = "Edad no válida"
            } else {
                tvResultado.text = "$nombre - $edad años"
 
                Toast.makeText(
                    this,
                    "Datos correctos",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }
}

Este ejemplo muestra cómo una Activity recoge los datos introducidos por el usuario, los valida y actualiza los componentes de la interfaz.

Una aplicación Android puede disponer de varias pantallas. Con XML Views, cada pantalla puede implementarse mediante una Activity asociada a un archivo XML. Por ejemplo:

MainActivity.kt      -> activity_main.xml
AlumnoActivity.kt    -> activity_alumno.xml

Para navegar entre Activities utilizamos principalmente los Intents.

Diseñar una nueva Activity

Desde Android Studio: File → New → Activity → Empty Views Activity

Android Studio crea:

  • Una clase Kotlin, por ejemplo AlumnoActivity.kt
  • Un archivo XML, por ejemplo activity_alumno.xml
  • La declaración correspondiente en AndroidManifest.xml

Una Activity sencilla tendría esta estructura:

class AlumnoActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        setContentView(R.layout.activity_alumno)
    }
}

Las Activities deben estar declaradas en AndroidManifest.xml. Por ejemplo, dentro de <application>:

<activity
    android:name=".AlumnoActivity"
    android:exported="false" />

Abrir una Activity desde la app

Un Intent es un objeto utilizado para solicitar acciones al sistema Android, como iniciar otra Activity. Para abrir AlumnoActivity desde MainActivity:

val intent = Intent(this, AlumnoActivity::class.java)
startActivity(intent)
  • this: Context de la Activity actual
  • AlumnoActivity::class.java: clase que queremos iniciar
  • startActivity(): inicia la nueva Activity

Normalmente utilizaremos esta operación dentro de un manejador de eventos:

val btnNuevo = findViewById<Button>(R.id.btnNuevo)
 
btnNuevo.setOnClickListener {
    val intent = Intent(this, AlumnoActivity::class.java)
    startActivity(intent)
}

Este es un Intent explícito, porque indicamos directamente la clase que queremos iniciar. También existen Intents implícitos, que permiten solicitar acciones a otras aplicaciones, como abrir una URL:

val intent = Intent(
    Intent.ACTION_VIEW,
    Uri.parse("https://abrilcode.com")
)
 
startActivity(intent)

Cerrar una Activity

Podemos cerrar una Activity mediante:

finish()

Por ejemplo, desde un botón de AlumnoActivity:

val btnVolver = findViewById<Button>(R.id.btnVolver)
 
btnVolver.setOnClickListener {
    finish()
}

Cuando se cierra una Activity, normalmente vuelve a mostrarse la anterior.

Pila de Activities

Android mantiene una pila de Activities denominada Back Stack.

Por ejemplo:

MainActivity
     |
     | startActivity()
     v
AlumnoActivity
     |
     | finish()
     v
MainActivity
  • Al abrir una Activity, normalmente se coloca encima de la anterior.
  • Cuando se cierra, la Activity anterior vuelve a mostrarse.
  • El botón o gesto Atrás del dispositivo también permite regresar normalmente a la pantalla anterior.

Para regresar a una Activity anterior, normalmente debemos utilizar finish() o la navegación Atrás.

No es recomendable crear otra instancia de la Activity anterior mediante startActivity(), ya que podríamos acumular pantallas repetidas.

Compartir información entre Activities

Cuando navegamos entre Activities, podemos necesitar enviar información a la nueva pantalla o recibir datos cuando esta termina. Android permite hacerlo mediante los extras de un Intent, que almacenan información asociada a una clave.

Enviar datos simples

El método putExtra() permite añadir valores a un Intent.

Por ejemplo, desde MainActivity:

val intent = Intent(this, AlumnoActivity::class.java)
 
intent.putExtra("nombre", "Ana")
intent.putExtra("edad", 20)
intent.putExtra("matriculado", true)
 
startActivity(intent)

En AlumnoActivity podemos recuperar esos valores:

val nombre = intent.getStringExtra("nombre")
val edad = intent.getIntExtra("edad", 0)
val matriculado = intent.getBooleanExtra("matriculado", false)
Tipo de dato Envío Recuperación
String putExtra(“nombre”, “Ana”) getStringExtra(“nombre”)
Int putExtra(“edad”, 20) getIntExtra(“edad”, 0)
Boolean putExtra(“activo”, true) getBooleanExtra(“activo”, false)
Double putExtra(“nota”, 8.5) getDoubleExtra(“nota”, 0.0)
Float putExtra(“altura”, 1.75f) getFloatExtra(“altura”, 0f)

En los métodos de recuperación de tipos numéricos y booleanos, el segundo parámetro indica el valor que se utilizará si no existe el dato.

Devolver datos a la Activity anterior

En ocasiones necesitamos abrir una Activity para recoger información y devolverla a la pantalla anterior. Por ejemplo:

MainActivity
     |
     | Abre el formulario
     v
AlumnoActivity
     |
     | Usuario introduce datos
     | Pulsa Guardar
     v
MainActivity
     |
     | Recibe nombre, edad y curso
     v
Procesa los datos

Para ello utilizaremos Activity Result API.

1. Registrar el receptor en MainActivity

Dentro de la clase MainActivity, pero fuera de onCreate(), declaramos:

private val alumnoLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 
   resultado ->
        if (resultado.resultCode == RESULT_OK) {
            //Este código se ejecuta al volver de la segunda activity
            val nombre = resultado.data?.getStringExtra("nombre")
            val edad = resultado.data?.getIntExtra("edad", 0)
            val curso = resultado.data?.getStringExtra("curso")
 
            Log.d("ALUMNO", "$nombre - $edad - $curso")
        }
    }

La lambda se ejecutará cuando la segunda Activity termine y devuelva un resultado.

Necesitamos importar:

import androidx.activity.result.contract.ActivityResultContracts

2. Abrir AlumnoActivity

En el manejador de evento del botón:

val intent = Intent(this, AlumnoActivity::class.java)
alumnoLauncher.launch(intent)

Utilizamos launch() porque queremos recibir un resultado cuando termine la segunda Activity.

3. Devolver el alumno desde AlumnoActivity

Suponemos que tenemos un formulario con etNombre, etEdad, etCurso y btnGuardar.

En el manejador del botón Guardar:

val nombre = etNombre.text.toString().trim()
val edad = etEdad.text.toString().toIntOrNull()
val curso = etCurso.text.toString().trim()
 
if (nombre.isNotEmpty() && edad != null && edad > 0
    && curso.isNotEmpty()) {
 
    val alumno = Alumno(nombre, edad, curso)
 
    val datos = Intent()
    datos.putExtra("nombre", nombre)
    datos.putExtra("edad", edad)
    datos.putExtra("curso", curso)
 
    setResult(RESULT_OK, datos)
    finish()
}
  • putExtra(): añade un dato al Intent, identificado por una clave
  • setResult(): establece el resultado que devolverá la Activity
  • RESULT_OK: indica que la operación terminó correctamente
  • finish(): cierra la Activity y permite entregar el resultado

Cuando se cierra AlumnoActivity, se ejecuta el callback registrado en MainActivity, que recupera los datos mediante getStringExtra() y getIntExtra().

Si el usuario cierra el formulario sin guardar, no recibiremos RESULT_OK.

Listas con RecyclerView

RecyclerView es un componente de Android que permite mostrar colecciones de datos mediante listas o cuadrículas. Su principal ventaja es que reutiliza las vistas de los elementos que dejan de estar visibles, evitando crear una vista nueva para cada elemento de la colección.

Por ejemplo, podemos utilizarlo para mostrar una lista de alumnos, productos, tareas o mensajes.

Elementos de RecyclerView

Para utilizar RecyclerView necesitamos varios elementos:

Elemento Función
RecyclerView Componente visual que muestra la colección
LayoutManager Diseño de cada elemento
Adapter Relaciona los datos de la colección con las vistas
ViewHolder Mantiene las referencias a los componentes visuales de cada elemento

Su funcionamiento puede resumirse así:

Lista de objetos
      |
      v
    Adapter
      |
      v
   ViewHolder
      |
      v
Layout XML de cada elemento
      |
      v
  RecyclerView

El Adapter utiliza los objetos de nuestra colección para rellenar las vistas correspondientes a cada elemento.

Añadir RecyclerView al proyecto

RecyclerView pertenece a la biblioteca AndroidX. Si no está incluida en el proyecto, debemos añadir la dependencia en app/build.gradle.kts:

dependencies {
    implementation("androidx.recyclerview:recyclerview:1.4.0")
}

Después sincronizamos Gradle.

Crear el modelo de datos

Vamos a mostrar una lista de objetos de la clase Alumno:

data class Alumno(
    val nombre: String,
    val edad: Int,
    val curso: String
)

Inicialmente utilizaremos una lista creada directamente en Kotlin.

Crear el activity

En activity_main.xml añadimos el componente:

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rvAlumnos"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Este ejemplo supone que RecyclerView ocupa el contenido de un contenedor sencillo, como un LinearLayout.

Diseño de cada elemento

Cada elemento de la lista tendrá su propio archivo XML. Creamos res/layout/item_alumno.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center_vertical"
    android:padding="12dp">
 
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">
 
        <TextView
            android:id="@+id/tvNombre"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:textStyle="bold" />
 
        <TextView
            android:id="@+id/tvDatos"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="14sp" />
 
    </LinearLayout>
 
    <Button
        android:id="@+id/btnEliminar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:text="Eliminar" />
 
</LinearLayout>

Este archivo define cómo se mostrará visualmente cada alumno.

Clases Adapter y ViewHolder

Para conectar nuestra lista de alumnos con RecyclerView necesitamos dos clases:

  • AlumnoViewHolder: mantiene las referencias a las vistas de cada elemento
  • AlumnoAdapter: relaciona los objetos de la lista con los ViewHolder y gestiona las actualizaciones

Clase AlumnoViewHolder

class AlumnoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
 
    val tvNombre: TextView = view.findViewById(R.id.tvNombre)
    val tvDatos: TextView = view.findViewById(R.id.tvDatos)
    val btnEliminar: Button = view.findViewById(R.id.btnEliminar)
 
    fun render(alumno: Alumno, onEliminar: (Alumno) -> Unit) {
 
        tvNombre.text = alumno.nombre
        tvDatos.text = "${alumno.edad} años - ${alumno.curso}"
 
        btnEliminar.setOnClickListener {
            onEliminar(alumno)
        }
    }
}

El ViewHolder tiene dos responsabilidades:

  • Mantener las referencias a los componentes visuales
  • Asignar los datos de un alumno y configurar el evento de su botón

El método render() recibe el alumno que debe mostrar y una función que se ejecutará cuando el usuario pulse Eliminar.

Observa que el ViewHolder no elimina directamente el alumno. Únicamente comunica qué objeto se quiere eliminar.

Crear la clase AlumnoAdapter

Creamos otro archivo llamado AlumnoAdapter.kt:

class AlumnoAdapter(
    private val alumnos: MutableList<Alumno>,
    private val onEliminar: (Alumno) -> Unit
) : RecyclerView.Adapter<AlumnoViewHolder>() {
 
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): AlumnoViewHolder {
 
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_alumno, parent, false)
 
        return AlumnoViewHolder(view)
    }
 
    override fun onBindViewHolder(
        holder: AlumnoViewHolder,
        position: Int
    ) {
        holder.render(alumnos[position], onEliminar)
    }
 
    override fun getItemCount(): Int {
        return alumnos.size
    }
 
    fun agregarAlumno(alumno: Alumno) {
        alumnos.add(alumno)
        notifyItemInserted(alumnos.lastIndex)
    }
 
    fun eliminarAlumno(alumno: Alumno) {
        val posicion = alumnos.indexOf(alumno)
 
        if (posicion != -1) {
            alumnos.removeAt(posicion)
            notifyItemRemoved(posicion)
        }
    }
}

Los tres métodos obligatorios del Adapter son:

Método Función
onCreateViewHolder() Crea la vista de un elemento cuando es necesaria
onBindViewHolder() Asocia un objeto con su ViewHolder
getItemCount() Devuelve el número de elementos de la lista

Además, hemos añadido dos métodos propios:

  • agregarAlumno(): añade un alumno y notifica su inserción
  • eliminarAlumno(): busca un alumno, lo elimina y notifica el cambio

El Adapter no decide cuándo debe eliminarse un alumno. Esa decisión la delegamos a la Activity mediante la función onEliminar.

Configurar RecyclerView desde la Activity

En MainActivity creamos una lista de alumnos, configuramos el LayoutManager y asociamos el Adapter.

class MainActivity : AppCompatActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        setContentView(R.layout.activity_main)
 
        val alumnos = mutableListOf(
            Alumno("Ana", 20, "DAM"),
            Alumno("Luis", 22, "DAW"),
            Alumno("Marta", 21, "DAM")
        )
 
        val recyclerView =
            findViewById<RecyclerView>(R.id.rvAlumnos)
 
        recyclerView.layoutManager = LinearLayoutManager(this)
 
        val adapter = AlumnoAdapter(alumnos) { alumno ->
            // Código que se ejecuta al pulsar Eliminar
        }
 
        recyclerView.adapter = adapter
    }
}

LinearLayoutManager organiza los elementos verticalmente por defecto.

Otros LayoutManager habituales son:

Eliminar elementos del RecyclerView

Ahora vamos a completar la función que recibe el evento del botón Eliminar. En MainActivity, modificamos la creación del Adapter:

val adapter = AlumnoAdapter(alumnos) { alumno ->
 
    // Aquí recibimos el alumno que queremos eliminar
}

Para eliminarlo necesitamos llamar al método eliminarAlumno() del Adapter.

Una forma sencilla de hacerlo es declarar el Adapter como propiedad de la Activity:

class MainActivity : AppCompatActivity() {
 
    private lateinit var adapter: AlumnoAdapter
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        setContentView(R.layout.activity_main)
 
        val alumnos = mutableListOf(
            Alumno("Ana", 20, "DAM"),
            Alumno("Luis", 22, "DAW"),
            Alumno("Marta", 21, "DAM")
        )
 
        val recyclerView =
            findViewById<RecyclerView>(R.id.rvAlumnos)
 
        recyclerView.layoutManager = LinearLayoutManager(this)
 
        adapter = AlumnoAdapter(alumnos) { alumno ->
            adapter.eliminarAlumno(alumno)
        }
 
        recyclerView.adapter = adapter
    }
}

Cuando el usuario pulsa Eliminar, ocurre lo siguiente:

Usuario pulsa Eliminar
          |
          v
AlumnoViewHolder
          |
          | onEliminar(alumno)
          v
AlumnoAdapter
          |
          | Ejecuta el callback
          v
MainActivity
          |
          | adapter.eliminarAlumno(alumno)
          v
AlumnoAdapter
          |
          | Elimina el objeto de la lista
          | notifyItemRemoved()
          v
RecyclerView actualiza la interfaz

De esta forma separamos las responsabilidades:

  • ViewHolder: captura el evento del botón
  • Adapter: mantiene y actualiza la colección
  • Activity: decide qué acción realizar cuando se pulsa Eliminar

Añadir elementos al RecyclerView

También podemos incorporar alumnos a la lista mediante el método que hemos definido en el Adapter:

adapter.agregarAlumno(
    Alumno("Pedro", 19, "DAM")
)

El método realiza dos operaciones:

alumnos.add(alumno)
notifyItemInserted(alumnos.lastIndex)

Primero modifica la colección y después informa a RecyclerView de que se ha añadido un elemento.

Notificar cambios al RecyclerView

Cuando modificamos la colección debemos informar al Adapter para que RecyclerView actualice la interfaz. Los principales métodos son:

Método Función
notifyItemInserted(posicion) Se ha añadido un elemento
notifyItemRemoved(posicion) Se ha eliminado un elemento
notifyItemChanged(posicion) Se ha modificado un elemento
notifyItemMoved(origen, destino) Un elemento ha cambiado de posición
notifyDataSetChanged() Solicita actualizar toda la lista

Siempre que sea posible, conviene utilizar los métodos que indican qué elementos han cambiado, en lugar de actualizar toda la lista.

En este ejemplo utilizamos indexOf(alumno) para localizar el elemento que queremos eliminar.

Si la lista puede contener alumnos con los mismos datos, conviene que cada alumno tenga un identificador único y localizarlo mediante ese identificador.

Arquitectura de aplicaciones Android

Persistencia de datos

Almacenamiento en ficheros

Serializar en un fichero JSON

data class Tarea(
    val nombre: String,
    val duracion: Int,
    val fecha: String
)
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
 
class TareaStorage(context: Context) {
 
    private val fichero = File(context.filesDir, "tareas.json")
 
    fun guardar(tareas: List<Tarea>) {
        val array = JSONArray()
 
        for (tarea in tareas) {
            val objeto = JSONObject().apply {
                put("nombre", tarea.nombre)
                put("duracion", tarea.duracion)
                put("fecha", tarea.fecha)
            }
 
            array.put(objeto)
        }
 
        fichero.writeText(array.toString())
    }
 
    fun cargar(): MutableList<Tarea> {
        if (!fichero.exists()) {
            return mutableListOf()
        }
 
        val array = JSONArray(fichero.readText())
        val tareas = mutableListOf<Tarea>()
 
        for (i in 0 until array.length()) {
            val objeto = array.getJSONObject(i)
 
            val tarea = Tarea(
                nombre = objeto.getString("nombre"),
                duracion = objeto.getInt("duracion"),
                fecha = objeto.getString("fecha")
            )
 
            tareas.add(tarea)
        }
 
        return tareas
    }
}

Guardar un fichero de propiedades

class Preferences(context : Context){
    companion object{
        const val PREFS_NAME = "myDatabase.prop"
        const val TASKS = "task_value"
    }
 
    val prefs : SharedPreferences = context.getSharedPreferences(PREFS_NAME, 0);
 
    fun saveInfo(tasks : List<String>){
        prefs.edit { putStringSet(TASKS, tasks.toSet()) }
    }
 
    fun getInfo() : MutableList<Tarea>{
        return prefs.getStringSet(TASKS, mutableSetOf<String>())?.toMutableList() ?: mutableListOf()
    }
}

Room

FireBase

Permisos y acceso a funcionalidades del dispositivo

Notificaciones y tareas en segundo plano

Desarrollo de una aplicación completa


© 2026 Fernando Valdeón

bloque1/moviles.txt · Última modificación: por fernando