====== 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.
{{ :bloque1:android_logo.png?200 |}}
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.
{{ :bloque1:android-studio.png?300|}}
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 |
{{ :bloque1:android-project.png?400 |}}
>Vista Android en el IDE
==== Gradle ====
{{ :bloque1:gradle-logo.png?200|}}
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 ====
{{ :bloque1:android-emulator.png?300|}}
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/guide/components/activities/activity-lifecycle?hl=es-419|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.
{{ :bloque1:activity_lifecycle.png?600 |}}
==== 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:
En el **AndroidManifest.xml**, se declara cuál es la actividad principal mediante un filtro de intención ('''') con la acción **MAIN** y la categoría **LAUNCHER**.
==== 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
Mi aplicaciónHola Android
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:
- 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''.
- 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:
{{ :bloque1:project-selection.png?600 |}}
* **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:
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:
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(R.id.etNombre)
val btnGuardar = findViewById
La función ''findViewById()'' 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(R.id.etNombre)
val tvResultado = findViewById(R.id.tvResultado)
val btnGuardar = findViewById
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
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(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(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(R.id.etNombre)
val etEdad = findViewById(R.id.etEdad)
val btnGuardar = findViewById
Este ejemplo muestra cómo una Activity recoge los datos introducidos por el usuario, los valida y actualiza los componentes de la interfaz.
===== Navegación entre Activities =====
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 '''':
==== 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
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
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:
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'':
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,
private val onEliminar: (Alumno) -> Unit
) : RecyclerView.Adapter() {
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(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(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) {
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 {
if (!fichero.exists()) {
return mutableListOf()
}
val array = JSONArray(fichero.readText())
val tareas = mutableListOf()
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){
prefs.edit { putStringSet(TASKS, tasks.toSet()) }
}
fun getInfo() : MutableList{
return prefs.getStringSet(TASKS, mutableSetOf())?.toMutableList() ?: mutableListOf()
}
}
==== Room ====
==== FireBase ====
===== Permisos y acceso a funcionalidades del dispositivo =====
===== Notificaciones y tareas en segundo plano =====
===== Desarrollo de una aplicación completa =====
----
(c) {{date> %Y}} Fernando Valdeón