# Programación Dirigida por Eventos

Grado en Ingeniería Informática · 2026/27 · Borja Miguel Peñuelas Morales, José Enrique Montero Prieto

Del cambio eléctrico en un pin al onClick de Android: fuentes, eventos y receptores; callbacks, listeners y despacho; Observer, pub/sub y MVVM; bucles de eventos y colas.

## Índice

- Tema 0: Entorno ESP32-C3 con VS Code y PlatformIO
- Tema 1: Fundamentos y origen de los eventos
- Tema 2: Callbacks, listeners, handlers y despacho
- Tema 3: Bucle de eventos, colas y dispatching
- Tema L1: Laboratorio · ESP32 emulado y Bluetooth

---

# Tema 0 · Entorno ESP32-C3 con VS Code y PlatformIO

Prepara y verifica Git, VS Code y PlatformIO en Windows o macOS, compila firmware real para la ESP32-C3 sin tener la placa y sigue el recorrido de un evento desde el pin GPIO hasta loop().

## Prepara tu entorno: Git, VS Code y PlatformIO

_Comprueba tu equipo, instala las herramientas en Windows (Git Bash) o macOS (zsh), entiende platformio.ini y compila la plantilla hasta obtener firmware.elf y firmware.bin. Termina diagnosticando los fallos típicos._

### Qué vas a montar hoy (y qué no) · Teoría

En esta asignatura vas a perseguir eventos a dos alturas muy distintas. En Android el sistema te entrega un `onClick` ya masticado; en un sistema empotrado puedes seguir el evento **desde el cambio eléctrico en un pin** hasta la interrupción, la rutina que la atiende y el código que acaba usando ese dato. Esta unidad deja listo el entorno para la parte empotrada.

La placa de referencia es la **ESP32-C3-DevKitM-1**, que PlatformIO identifica como `esp32-c3-devkitm-1`. Su microcontrolador tiene un núcleo **RISC-V de 32 bits**, Wi-Fi y Bluetooth Low Energy.

<div class="flow">
  <div><b>Git</b><small>y Git Bash en Windows</small></div>
  <div><b>VS Code</b><small>el editor</small></div>
  <div><b>PlatformIO</b><small>extensión + comando pio</small></div>
  <div><b>Plantilla</b><small>pde-esp32c3-eventos</small></div>
  <div><b>pio run</b><small>SUCCESS + firmware</small></div>
</div>

> [!IMPORTANTE] Primera sesión: solo el ordenador
> No necesitas la ESP32-C3, ni cable USB, ni componentes. La carga por USB, el puerto serie y el botón BOOT se prueban más adelante, con la placa delante.

### Anfitrión frente a objetivo

La idea clave del día es separar dos máquinas:

- **Anfitrión (host)**: tu Windows o tu Mac, donde escribes y compilas.
- **Objetivo (target)**: el núcleo RISC-V de la ESP32-C3, donde se ejecutará el firmware.

Compilas en un sitio para ejecutar en otro. Por eso hace falta una **toolchain** específica (compilador, ensamblador, enlazador y utilidades para RISC-V), y por eso **compilar sin placa es una prueba real**: valida el código, el framework, el compilador y el enlazado. Lo único que queda pendiente es la interacción con el hardware.

![En el anfitrión (tu Windows o tu Mac), tu código src/main.cpp pasa por pio run, que usa la toolchain RISC-V (compilador, ensamblador, enlazador y utilidades) para producir firmware.elf, con símbolos para depurar, y firmware.bin, la imagen para la flash. Una flecha discontinua lleva firmware.bin hasta el objetivo, la ESP32-C3 con núcleo RISC-V de 32 bits, donde se cargará por USB en otra sesión.](/img/pde/tema-0/anfitrion-objetivo.svg "Compilas en el anfitrión para ejecutar en el objetivo: hoy llegas hasta firmware.bin")

> [!ANALOGIA]
> Es como imprimir un libro en una imprenta de Madrid para venderlo en Buenos Aires: puedes comprobar que el libro está bien impreso sin haber viajado. Lo que no sabrás hasta llegar es cómo lo reciben los lectores.

**Preguntas**

**1.** Compilas el proyecto en tu portátil con Windows. ¿Para qué arquitectura genera código PlatformIO?

   a) Para x86-64, la de tu portátil, y luego la placa lo traduce
   b) Para el núcleo RISC-V de 32 bits de la ESP32-C3
   c) Para ARM, porque todos los microcontroladores son ARM
   d) Para ninguna concreta: el firmware es bytecode portable

**2.** ¿Qué **sí** queda comprobado cuando la compilación termina en SUCCESS sin placa conectada?

   a) Que el compilador y el enlazado funcionan
   b) Que el botón BOOT genera interrupciones
   c) Que el framework Arduino-ESP32 está resuelto
   d) Que el puerto serie de la placa responde
   e) Que el código fuente no tiene errores de compilación

### Comprueba tu equipo y aprende las reglas · Práctica

Compilar para una ESP32-C3 pide pocos recursos. Lo esencial: que VS Code sea compatible con tu sistema, espacio para los paquetes de PlatformIO e **Internet durante la primera instalación**.

| Componente | Mínimo | Recomendado |
| --- | --- | --- |
| Windows | Windows 10/11 de 64 bits | Windows 11 o 10 actualizado |
| macOS | Versión compatible con VS Code actual | macOS 13 o posterior, Apple Silicon o Intel |
| Memoria / disco | 4 GB de RAM y 5 GB libres | 8 GB de RAM y unos 10 GB libres |
| Red | HTTPS durante la instalación | Buena conexión la primera vez; luego se usa la caché |

**Autodiagnóstico en dos minutos.** En Windows: `Win + R` → `msinfo32` → «Tipo de sistema», y el espacio libre en *Este equipo*. En macOS: menú Apple → «Acerca de este Mac» y *Ajustes del Sistema → General → Almacenamiento*. **No necesitas virtualización** para compilar.

| Caso | Tu equipo | Qué haces |
| --- | --- | --- |
| A | Windows x64 o Mac reciente, ≥ 8 GB, SSD | Instalación estándar (ruta recomendada) |
| B | Windows ARM64 o arquitectura poco habitual | Instaladores de **tu** arquitectura; PlatformIO resuelve luego la toolchain del objetivo |
| C | Poco disco o red muy limitada | Git y VS Code primero; PlatformIO y la 1.ª compilación desde una red mejor |

### Reglas que evitan casi todos los problemas

1. Misma plantilla y mismo `platformio.ini` que el resto de la clase: **no cambies** `platform`, `board` ni `framework`.
2. En Windows, usa **Git Bash** para los comandos: así la sintaxis es casi la de macOS.
3. Rutas simples, sin tildes: `C:\dev\pde` en Windows, `~/dev/pde` en macOS.
4. **No instales a mano** GCC para RISC-V, esptool ni Arduino-ESP32: PlatformIO gestiona versiones compatibles en `~/.platformio`.
5. No borres `~/.platformio` para «arreglar» algo sin saber la causa: tendrías que volver a descargarlo todo.
6. Hoy **no** lances `pio run -t upload` ni `pio device monitor`: sin placa no se pueden verificar.
7. Distingue **compilación** de **ejecución**.

**Preguntas**

**3.** ¿Qué carpeta de trabajo recomienda la guía en Windows?

   a) `C:\Users\<tú>\OneDrive\Documentos\Prácticas PDE`
   b) `C:\Program Files\pde`
   c) `C:\dev\pde`
   d) El Escritorio, para tenerla a mano

**4.** Marca las acciones que **van contra** las reglas de esta unidad.

   a) Instalar a mano el compilador GCC para RISC-V
   b) Usar Git Bash para los comandos en Windows
   c) Cambiar `board` en `platformio.ini` porque «suena mejor otra placa»
   d) Ejecutar `pio run -t upload` hoy para ver qué pasa
   e) Dejar que PlatformIO gestione sus paquetes en `~/.platformio`

### Instalación en Windows con Git Bash · Práctica

En Windows usamos **Git for Windows** porque, además de Git, trae **Git Bash**, una terminal con sintaxis tipo Unix. Luego instalamos VS Code y, desde el propio editor, la extensión PlatformIO IDE.

1. **Git.** Descarga el instalador para tu arquitectura desde `https://git-scm.com/download/win` y deja las opciones por defecto (el editor predeterminado da igual).
2. Abre *Inicio → Git Bash* y comprueba:

```bash
git --version
```

3. **VS Code** desde `https://code.visualstudio.com` («User Installer» basta). Marca **Add to PATH** si aparece. Cierra y vuelve a abrir Git Bash.
4. Comprueba:

```bash
code --version
```

5. **PlatformIO IDE.** En VS Code, `Ctrl + Shift + X`, busca «PlatformIO IDE», verifica que el publicador es **PlatformIO** y pulsa *Install*. Espera a que se pueda abrir **PIO Home**.
6. La extensión instala su *Core* en `~/.platformio`. Para usar `pio` fuera de VS Code, añade su carpeta **Scripts** al PATH de Bash:

```bash
echo 'export PATH="$HOME/.platformio/penv/Scripts:$PATH"' >> ~/.bashrc
source ~/.bashrc
pio --version
```

7. Si `pio` sigue sin existir, abre una vez PIO Home, espera a que termine y abre una Git Bash **nueva**.

> [!TRUCO] La regla del «cierra y abre»
> Una terminal lee el PATH al arrancar. Si instalas algo con la terminal abierta, esa terminal no se entera. Ante un `command not found` justo después de instalar, lo primero es abrir una terminal nueva.

La instalación está lista cuando `git --version`, `code --version` y `pio --version` responden. En Windows ARM64 usa instaladores ARM64; no hace falta activar Hyper-V, WHPX ni virtualización.

**Preguntas**

**5.** En Windows, ¿qué carpeta de PlatformIO se añade al PATH en `~/.bashrc`?

   a) `$HOME/.platformio/penv/bin`
   b) `$HOME/.platformio/packages`
   c) `$HOME/.vscode/extensions/platformio`
   d) `$HOME/.platformio/penv/Scripts`

**6.** ¿Qué comando escribes en Git Bash para comprobar que PlatformIO Core es accesible?

### Instalación en macOS con zsh · Práctica

En macOS Git llega con las **Command Line Tools** de Apple y la terminal por defecto es **zsh**.

1. **Git.** En Terminal:

```bash
xcode-select --install
```

Pulsa «Instalar», acepta la licencia y espera. Si dice que ya están instaladas, sigue. Después: `git --version`.

2. **VS Code** desde `https://code.visualstudio.com`, versión **Apple Silicon** o **Intel** según tu equipo. Arrástralo a *Aplicaciones*.
3. Para tener el comando `code`: en VS Code, `Cmd + Shift + P` → escribe «shell command» → **Shell Command: Install 'code' command in PATH**. Cierra y abre Terminal y prueba `code --version`.
4. **PlatformIO IDE.** `Cmd + Shift + X`, busca «PlatformIO IDE» del publicador PlatformIO, *Install*, y espera a que termine PlatformIO Core.
5. Añade PlatformIO al PATH de zsh:

```bash
echo 'export PATH="$HOME/.platformio/penv/bin:$PATH"' >> ~/.zprofile
source ~/.zprofile
pio --version
```

6. Si `pio` no aparece, abre PIO Home, espera a que se inicialice, abre una Terminal nueva y repite.

### Windows frente a macOS, de un vistazo

| | Windows | macOS |
| --- | --- | --- |
| Terminal | Git Bash | Terminal (zsh) |
| Git viene de | Git for Windows | Command Line Tools (`xcode-select --install`) |
| Comando `code` | Opción *Add to PATH* del instalador | *Shell Command: Install 'code' command in PATH* |
| Fichero de perfil | `~/.bashrc` | `~/.zprofile` |
| Carpeta de `pio` | `~/.platformio/penv/Scripts` | `~/.platformio/penv/bin` |

> [!NOTA]
> Hoy no conectas ninguna placa, así que tampoco tienes que autorizar dispositivos USB en macOS.

**Preguntas**

**7.** ¿Qué comando instala Git en macOS mediante las Command Line Tools de Apple?

**8.** Una compañera en macOS añade la línea del PATH de PlatformIO a `~/.bashrc` y `pio` sigue sin aparecer en Terminal. ¿Qué ha pasado?

   a) macOS usa zsh por defecto, que lee `~/.zprofile`, no `~/.bashrc`
   b) En macOS PlatformIO no se puede usar fuera de VS Code
   c) Tenía que usar `penv/Scripts` en lugar de `penv/bin`
   d) Le falta instalar Git Bash para macOS

### platformio.ini: la fuente de verdad · Teoría

PlatformIO separa tres conceptos que conviene no mezclar:

<div class="cards">
  <div class="card"><h4>platform</h4><p>El ecosistema de soporte del fabricante. Aquí, <code>espressif32</code>, fijado a una versión.</p></div>
  <div class="card"><h4>board</h4><p>Una placa concreta: su microcontrolador y parámetros habituales. Aquí, <code>esp32-c3-devkitm-1</code>.</p></div>
  <div class="card"><h4>framework</h4><p>La capa de software con la que programas. Aquí, <code>arduino</code> (Arduino-ESP32).</p></div>
</div>

La plantilla de la asignatura trae exactamente esto:

```ini
[env:esp32-c3-devkitm-1]
platform = espressif32@7.0.1
board = esp32-c3-devkitm-1
framework = arduino
monitor_speed = 115200
```

Léelo línea a línea como **decisiones reproducibles**:

- `[env:...]` da **nombre al entorno**. Ese nombre aparece luego en la salida de `pio run` y en la carpeta `.pio/build/esp32-c3-devkitm-1/`.
- `platform = espressif32@7.0.1` fija la **versión** de la plataforma, para que toda la clase resuelva el mismo conjunto de paquetes.
- `board` selecciona la ESP32-C3-DevKitM-1.
- `framework = arduino` activa la **API** Arduino-ESP32. **No** significa que uses el IDE de Arduino.
- `monitor_speed = 115200` deja preparado el monitor serie para cuando haya placa.

> [!EXAMEN]
> Distinguir *platform*, *board* y *framework* es pregunta de manual: plataforma = soporte del fabricante, placa = hardware concreto, framework = API con la que escribes.

### Cómo llega la toolchain

No descargas ningún compilador RISC-V. Al procesar el proyecto, el **gestor de paquetes** de PlatformIO resuelve plataforma, toolchain, framework y utilidades, y lo guarda en `~/.platformio`:

```bash
pio pkg install   # instala o completa los paquetes del proyecto
pio pkg list      # muestra lo que ha quedado resuelto
pio project config  # cómo interpreta PlatformIO tu platformio.ini
pio boards esp32-c3 # placas cuyo identificador contiene esp32-c3
```

La primera vez tarda varios minutos y descarga cientos de megabytes; después se reutiliza la caché.

**Preguntas**

**9.** ¿Qué significa `framework = arduino` en nuestra plantilla?

   a) Que hay que compilar desde el IDE de Arduino en vez de VS Code
   b) Que la placa objetivo es una Arduino Uno
   c) Que el programa se escribe con la API Arduino-ESP32
   d) Que se usa la toolchain AVR de Arduino

**10.** ¿Por qué la plantilla escribe `espressif32@7.0.1` y no simplemente `espressif32`?

   a) Porque sin versión PlatformIO no sabe que es una placa Espressif
   b) Para que toda la clase resuelva el mismo conjunto de paquetes y los errores sean reproducibles
   c) Porque la versión 7.0.1 es la única que soporta RISC-V
   d) Para que la compilación sea más rápida

**11.** Escribe el identificador de `board` que usa la plantilla.

### Carpeta de trabajo y proyecto plantilla · Práctica

Partimos de una **plantilla común** en lugar de crear el proyecto desde cero: así todos compilamos para la misma placa, plataforma y framework, y nadie obtiene resultados distintos por haber creado el proyecto otro día.

La plantilla es `pde-esp32c3-eventos.zip` y contiene `platformio.ini`, `src/main.cpp` y un `README.md`. **No** trae la toolchain ni binarios: eso lo resuelve PlatformIO en tu equipo.

### 1. Crea la carpeta

```bash
# Windows · Git Bash
mkdir -p /c/dev/pde
cd /c/dev/pde
pwd

# macOS · Terminal
mkdir -p ~/dev/pde
cd ~/dev/pde
pwd
```

> [!CUIDADO]
> No uses OneDrive, iCloud Drive ni otra carpeta sincronizada: los sincronizadores e indexadores pueden bloquear los ficheros temporales de compilación.

### 2. Descomprime y abre

Copia el ZIP a esa carpeta y extráelo (Windows: clic derecho → «Extraer todo…»; macOS: doble clic). Luego:

```bash
cd /c/dev/pde/pde-esp32c3-eventos   # en macOS: cd ~/dev/pde/pde-esp32c3-eventos
code .
```

Si PlatformIO reconoce `platformio.ini`, la barra inferior muestra **Build**, **Upload** y **Serial Monitor**. Hoy solo usas Build.

### 3. Reconoce la estructura

```text
pde-esp32c3-eventos/
├── platformio.ini
├── README.md
└── src/
    └── main.cpp
```

Al compilar aparecerá además **`.pio`**: paquetes y resultados generados. **No la edites ni la entregues.** Antes de compilar, confirma que estás en la raíz:

```bash
pwd
ls      # debe aparecer platformio.ini
```

Si `ls` no muestra `platformio.ini`, estás un nivel por encima o por debajo y `pio run` no sabrá qué proyecto construir.

**Preguntas**

**12.** Estás en la raíz del proyecto en la terminal. ¿Qué comando abre VS Code en la carpeta actual?

**13.** Tras compilar aparece la carpeta `.pio`. ¿Qué haces con ella al entregar la práctica?

   a) La entregas: contiene el firmware y es lo que se evalúa
   b) No la entregas ni la editas: es contenido generado automáticamente
   c) La renombras a `build` para que sea más claro
   d) La copias dentro de `src/` para que el proyecto sea autocontenido

### Primera compilación sin placa · Práctica

La compilación **no necesita dispositivo**: el objetivo ya está descrito en `platformio.ini`. Desde la raíz del proyecto:

```bash
pio pkg install
pio run
```

La primera vez descargará paquetes y luego compilará y enlazará. Al final debe aparecer **SUCCESS** para el entorno `esp32-c3-devkitm-1`.

### Los artefactos

```bash
ls -lh .pio/build/esp32-c3-devkitm-1/
```

| Fichero | Qué es |
| --- | --- |
| `firmware.elf` | Conserva información útil para el **enlazado y la depuración** (símbolos, secciones). |
| `firmware.bin` | La **imagen binaria** que, en otra sesión, se cargará en la memoria flash de la placa. |

De forma simplificada, `pio run` coordina: fuente C/C++ → preprocesado → compilación → objetos → enlazado → firmware. Tú no invocas `gcc`, el enlazador ni `esptool` a mano.

### Comandos útiles

```bash
pio run -t clean   # limpia la compilación sin borrar las dependencias descargadas
pio run            # vuelve a compilar desde cero
pio run -v         # verbose: compilador, flags, cabeceras y paquetes reales
```

La salida `-v` es enorme: úsala solo cuando necesites saber qué está usando PlatformIO de verdad. Desde la interfaz, lo mismo es *PlatformIO (barra lateral) → Project Tasks → esp32-c3-devkitm-1 → General → Build*.

> [!IMPORTANTE]
> No ejecutes `pio run -t upload` ni `pio device monitor`. Es **normal** que no haya puerto serie ni aparezca ninguna ESP32-C3 en `pio device list`. El objetivo de hoy termina en **SUCCESS** y en `firmware.elf` / `firmware.bin`.

### Verificación completa

```bash
git --version && code --version && pio --version && pio system info
pio project config
pio pkg install
pio pkg list
pio run
ls -lh .pio/build/esp32-c3-devkitm-1/
```

**Preguntas**

**14.** ¿Cuál de los dos artefactos es la imagen que se cargará en la flash de la placa?

   a) `firmware.elf`, porque incluye los símbolos de depuración
   b) `platformio.ini`, que describe el entorno
   c) `firmware.bin`, la imagen binaria
   d) `main.cpp`, que se compila en la propia placa

**15.** Ordena la secuencia de verificación dentro de la raíz del proyecto.

   Ordena: `ls -lh .pio/build/esp32-c3-devkitm-1/` · `pio pkg install` · `pio pkg list` · `pio project config` · `pio run`

**16.** ¿Qué comando limpia la compilación **sin** borrar las dependencias descargadas?

### Diagnóstico I: «command not found» y carpetas · Ejercicio

Cuando algo falla, **busca el primer error significativo**: los mensajes posteriores suelen ser consecuencia de él. Esta es la parte de la tabla de problemas que tiene que ver con la terminal, el PATH y la carpeta en la que estás.

| Síntoma | Causa probable | Solución |
| --- | --- | --- |
| `git: command not found` | Git no instalado o terminal abierta antes de instalarlo | Windows: Git for Windows + Git Bash nueva. macOS: `xcode-select --install` |
| `code: command not found` | VS Code no está en el PATH | Windows: reinstalar con *Add to PATH* o terminal nueva. macOS: *Shell Command: Install 'code' command in PATH* |
| `pio: command not found` | PlatformIO Core no está en el PATH o la extensión no terminó | Abrir PIO Home, esperar y aplicar el PATH del apartado de tu sistema |
| `Not a PlatformIO project` / no `platformio.ini` | Ejecutas `pio` en otra carpeta | `pwd` y `ls`; entrar en `pde-esp32c3-eventos` |

> [!TRUCO] Método en tres preguntas
> 1. ¿El programa está **instalado**?
> 2. ¿La terminal **lo encuentra** (PATH, terminal nueva)?
> 3. ¿Estoy en la **carpeta** correcta?

Ahora tú. Lee cada caso, decide la causa y la primera acción.

<details><summary>Caso de ejemplo resuelto</summary>
<p>Javier instala VS Code con Git Bash ya abierta y escribe <code>code --version</code>: <code>command not found</code>. Causa: la terminal leyó el PATH antes de la instalación. Acción: cerrar Git Bash y abrir una nueva. Si persiste, reinstalar marcando <i>Add to PATH</i>.</p>
</details>

**Preguntas**

**17.** Lucía instaló la extensión PlatformIO IDE hace un minuto. `git --version` y `code --version` responden, pero `pio --version` da `command not found`. ¿Qué haces primero?

   a) Borrar `~/.platformio` y reinstalar la extensión
   b) Instalar Python y PlatformIO Core a mano con pip
   c) Abrir PIO Home, esperar a que termine, aplicar el PATH de PlatformIO y abrir una terminal nueva
   d) Cambiar `platform` en `platformio.ini`

**18.** Marcos ejecuta `pio run` en `/c/dev/pde` y obtiene `Not a PlatformIO project`. ¿Cuál es la causa?

   a) Está una carpeta por encima: `platformio.ini` está en `pde-esp32c3-eventos`
   b) Le falta ejecutar `pio pkg install`
   c) El `board` está mal escrito
   d) No tiene la placa conectada

**19.** ¿Qué comando muestra la carpeta en la que está tu terminal ahora mismo?

### Diagnóstico II: red, disco, permisos y falsas alarmas · Ejercicio

La segunda mitad de la tabla trata de lo que pasa **dentro** del proyecto: paquetes, red, disco y hardware.

| Síntoma | Causa probable | Solución |
| --- | --- | --- |
| `Unknown board ID` | `board` incorrecto o plataforma no resuelta | `board = esp32-c3-devkitm-1` y `pio pkg install` |
| `Could not install package` / HTTP error | Red, proxy, DNS o firewall | Otra red; revisar proxy; repetir `pio pkg install` |
| `No space left on device` | Sin espacio para paquetes o temporales | Liberar varios GB y recompilar |
| `Permission denied` en `~/.platformio` | Carpeta creada con otro usuario | **No** ejecutar PlatformIO como administrador/root; corregir permisos |
| Error de compilación en `main.cpp` | Código modificado o incompleto | Restaurar `src/main.cpp` del ZIP y `pio run` |
| No aparece ningún puerto serie | No hay placa | **Normal hoy**: no es un fallo |
| `Upload failed` / device not found | Se intentó cargar sin hardware | No usar `upload` hasta tener placa |

### Red del campus y proxy

Si la descarga va en casa pero falla en el campus, **no toques `platformio.ini`**: el problema es de conectividad, no de la placa. Si el centro usa un proxy, se configura de forma temporal en la terminal con los datos reales que te den:

```bash
export HTTP_PROXY=http://proxy.ejemplo:8080
export HTTPS_PROXY=http://proxy.ejemplo:8080
pio pkg install
```

No dejes contraseñas en documentos compartidos ni en el historial. Si una descarga queda a medias, vuelve a lanzar `pio pkg install`: PlatformIO detecta lo que falta y continúa.

### Si tu equipo no puede

PlatformIO Core desde Python (avanzado, no recomendado para la clase), **Wokwi** (simulador web con ESP32, GPIO, botones e interrupciones), un equipo del aula, o hacer la primera descarga en otra red y conservar el perfil de usuario.

**Preguntas**

**20.** En casa `pio pkg install` funciona; en la Wi-Fi del campus falla con `HTTP error`. Un compañero sugiere cambiar `platform` a otra versión. ¿Qué respondes?

   a) Buena idea: otra versión puede estar en un servidor distinto
   b) Mejor cambiar `board`, que es lo que descarga los paquetes
   c) Hay que borrar `~/.platformio` y empezar de cero
   d) Es un problema de conectividad: revisar proxy u otra red y repetir `pio pkg install`, sin tocar `platformio.ini`

**21.** En esta primera sesión sin placa, ¿qué situaciones son **normales** y no indican fallo del entorno?

   a) `pio device list` no muestra ninguna ESP32-C3
   b) `pio run` termina con `No space left on device`
   c) No aparece ningún puerto serie
   d) `pio run -t upload` falla con *device not found*
   e) `Unknown board ID` al compilar

**22.** Te sale `Permission denied` en `~/.platformio`. ¿Qué **no** debes hacer?

   a) Ejecutar PlatformIO como administrador o con `sudo`
   b) Revisar los permisos de tu carpeta de usuario
   c) Comprobar con qué usuario se creó la carpeta
   d) Leer el primer error significativo del registro

### Repaso: chuleta del entorno · Repaso

> [!RESUMEN] El entorno en cinco pasos
> 1. Instala **Git** y **VS Code**.
> 2. Añade **PlatformIO IDE**.
> 3. Haz accesible **pio** en el PATH.
> 4. Abre la plantilla con su `platformio.ini`.
> 5. `pio pkg install` + `pio run` → **SUCCESS**, `firmware.elf` y `firmware.bin`.

| Concepto | Lo que debes recordar |
| --- | --- |
| Anfitrión / objetivo | Compilas en Windows/macOS; el código es para el RISC-V de la ESP32-C3 |
| `platformio.ini` | Fuente de verdad: `env`, `platform`, `board`, `framework`, `monitor_speed` |
| Toolchain | La resuelve PlatformIO en `~/.platformio`; nunca a mano |
| PATH de `pio` | Windows: `penv/Scripts` en `~/.bashrc`. macOS: `penv/bin` en `~/.zprofile` |
| Artefactos | `.pio/build/esp32-c3-devkitm-1/firmware.elf` y `firmware.bin` |
| Hoy no | `upload`, `device monitor`, puerto serie, BOOT |

**Lista final:** `git`, `code` y `pio` responden · ruta simple (`C:\dev\pde` o `~/dev/pde`) · VS Code reconoce `platformio.ini` · `pio project config` muestra el entorno · `pio pkg install` y `pio pkg list` correctos · `pio run` en SUCCESS · existen los dos artefactos.

**Preguntas**

**23.** «Si compila, el botón funciona.» ¿Es correcta la afirmación?

   a) Sí: si el código compila, el comportamiento eléctrico está garantizado
   b) No: compilar valida la cadena de herramientas y el código, pero el comportamiento eléctrico solo se verifica con la placa
   c) Sí, siempre que `monitor_speed` sea 115200
   d) No: compilar sin placa no demuestra nada

**24.** ¿Qué comando instala o completa los paquetes del proyecto (y retoma una descarga interrumpida)?

**25.** ¿Qué claves de `platformio.ini` **no** debes cambiar salvo indicación expresa del profesor?

   a) `platform`
   b) `board`
   c) `framework`
   d) Comentarios que añadas tú

### Solucionario

1. b) Para el núcleo RISC-V de 32 bits de la ESP32-C3  
   El anfitrión (Windows/macOS) solo compila. El código generado es para el **objetivo**: el núcleo RISC-V de la ESP32-C3. Por eso PlatformIO descarga una toolchain RISC-V.
2. a) Que el compilador y el enlazado funcionan; c) Que el framework Arduino-ESP32 está resuelto; e) Que el código fuente no tiene errores de compilación  
   Compilar valida la cadena de herramientas: código, framework, compilador y enlazado. El comportamiento eléctrico (botón, interrupciones reales) y el puerto serie solo se verifican con la placa.
3. c) `C:\dev\pde`  
   `C:\dev\pde` es corta, sin tildes ni espacios y no está sincronizada. OneDrive o iCloud pueden bloquear ficheros temporales de compilación.
4. a) Instalar a mano el compilador GCC para RISC-V; c) Cambiar `board` en `platformio.ini` porque «suena mejor otra placa»; d) Ejecutar `pio run -t upload` hoy para ver qué pasa  
   La toolchain la gestiona PlatformIO, el `platformio.ini` es común a toda la clase y hoy no se carga nada en la placa. Git Bash y dejar a PlatformIO gestionar `~/.platformio` son justo lo recomendado.
5. d) `$HOME/.platformio/penv/Scripts`  
   En Windows el entorno virtual de PlatformIO deja los ejecutables en `penv/Scripts`. En macOS la carpeta equivalente es `penv/bin`.
6. `pio --version`  
   `pio --version` debe mostrar una versión. Junto con `git --version` y `code --version`, es el criterio de que la instalación está lista.
7. `xcode-select --install`  
   `xcode-select --install` abre la ventana del sistema que instala las Command Line Tools, que incluyen Git.
8. a) macOS usa zsh por defecto, que lee `~/.zprofile`, no `~/.bashrc`  
   zsh no lee `~/.bashrc`. La guía usa `~/.zprofile` con `penv/bin`. `penv/Scripts` es la carpeta de Windows y Git Bash solo existe allí.
9. c) Que el programa se escribe con la API Arduino-ESP32  
   `framework = arduino` selecciona Arduino-ESP32 como API de programación. Seguimos en VS Code + PlatformIO y la placa sigue siendo la ESP32-C3.
10. b) Para que toda la clase resuelva el mismo conjunto de paquetes y los errores sean reproducibles  
   Fijar la versión hace que todos obtengan las mismas versiones de plataforma y paquetes, así un error es reproducible en cualquier equipo de la clase.
11. `esp32-c3-devkitm-1`  
   `board = esp32-c3-devkitm-1`, el identificador de PlatformIO para la ESP32-C3-DevKitM-1.
12. `code .`  
   `code .` abre VS Code con la carpeta actual como proyecto (el punto es «aquí»).
13. b) No la entregas ni la editas: es contenido generado automáticamente  
   `.pio` contiene paquetes y resultados generados. Se regenera con `pio run`; no forma parte del código fuente.
14. c) `firmware.bin`, la imagen binaria  
   `firmware.bin` es la imagen binaria para la flash. `firmware.elf` conserva información para enlazado y depuración.
15. 1. `pio project config` → 2. `pio pkg install` → 3. `pio pkg list` → 4. `pio run` → 5. `ls -lh .pio/build/esp32-c3-devkitm-1/`  
   Primero ves cómo interpreta PlatformIO la configuración, luego resuelves y listas paquetes, compilas y, por último, compruebas que existen los artefactos.
16. `pio run -t clean`  
   `pio run -t clean` borra los resultados de compilación; los paquetes de `~/.platformio` se conservan.
17. c) Abrir PIO Home, esperar a que termine, aplicar el PATH de PlatformIO y abrir una terminal nueva  
   La causa probable es que el Core no está en el PATH o no terminó de instalarse. Borrar `~/.platformio` sin diagnóstico va contra las reglas, y `platformio.ini` no tiene nada que ver con que la terminal encuentre `pio`.
18. a) Está una carpeta por encima: `platformio.ini` está en `pde-esp32c3-eventos`  
   En `/c/dev/pde` solo está la carpeta del proyecto, no `platformio.ini`. Hay que hacer `cd pde-esp32c3-eventos` y repetir.
19. `pwd`  
   `pwd` (*print working directory*) muestra la ruta actual; `ls` lista lo que hay dentro.
20. d) Es un problema de conectividad: revisar proxy u otra red y repetir `pio pkg install`, sin tocar `platformio.ini`  
   La guía insiste: un fallo de red no se arregla editando `platformio.ini`. Se revisa proxy/red y se repite `pio pkg install`, que reanuda lo que falte.
21. a) `pio device list` no muestra ninguna ESP32-C3; c) No aparece ningún puerto serie; d) `pio run -t upload` falla con *device not found*  
   Sin placa es normal que no haya puerto ni dispositivo, y que `upload` falle (por eso no se lanza hoy). Falta de espacio y `Unknown board ID` sí son problemas reales del entorno.
22. a) Ejecutar PlatformIO como administrador o con `sudo`  
   Ejecutar como administrador/root suele ser la causa del problema (carpetas creadas con otro propietario). La solución es corregir los permisos de tu carpeta de usuario.
23. b) No: compilar valida la cadena de herramientas y el código, pero el comportamiento eléctrico solo se verifica con la placa  
   Hay que distinguir compilación de ejecución. Compilar sin placa es una comprobación real (código, framework, compilador, enlazado), pero no prueba el hardware.
24. `pio pkg install`  
   `pio pkg install` resuelve plataforma, toolchain y framework; si algo quedó a medias, descarga solo lo que falta.
25. a) `platform`; b) `board`; c) `framework`  
   `platform`, `board` y `framework` se fijan para toda la clase y garantizan una construcción reproducible.

## Del pin al loop(): el evento sin hardware

_Sigue en el código de la plantilla el recorrido señal → GPIO → interrupción → ISR → estado compartido → loop(): GPIO9 y BOOT, INPUT_PULLUP, flanco FALLING, attachInterrupt, volatile y antirrebote, y compáralo con un onClick de Android._

### Un evento empieza mucho antes del callback · Teoría

Cuando en Android se ejecuta tu `onClick`, el sistema ya ha hecho muchísimo trabajo por ti: el táctil detectó el dedo, el sistema decidió a qué vista pertenece, la gestionó el hilo principal... Tú solo ves el final.

En un microcontrolador ese trabajo **no está escondido**. Puedes seguir el evento desde que cambia la tensión de un pin hasta que tu código decide qué hacer. Y aparece el mismo esquema que en cualquier sistema dirigido por eventos:

<div class="flow">
  <div><b>Suceso</b><small>algo cambia fuera</small></div>
  <div><b>Detección</b><small>alguien lo nota</small></div>
  <div><b>Manejador</b><small>código que reacciona</small></div>
  <div><b>Estado</b><small>queda constancia</small></div>
  <div><b>Consumo</b><small>se interpreta y actúa</small></div>
</div>

| | Android (`onClick`) | ESP32-C3 (GPIO) |
| --- | --- | --- |
| Nivel | Alto: muchas capas ya han procesado la entrada | Bajo: ves el pin, la interrupción y la rutina |
| Manejador | Un método normal en el hilo principal | Una **ISR**, que interrumpe lo que se estuviera ejecutando |
| Duración permitida | Corta, para no congelar la interfaz | **Extremadamente** corta |

> [!NOTA]
> La relación con Android es **conceptual**: comparten el patrón, no el mecanismo. Una ISR trabaja muy por debajo de un listener y tiene reglas mucho más estrictas.

Hoy no hay placa, así que **no** podemos medir la parte eléctrica real: el nivel de tensión, el rebote del pulsador o el instante exacto en que el periférico pide la interrupción. Lo que sí podemos es leer el código y saber qué configura cada etapa cuando el firmware se ejecute.

**Preguntas**

**1.** Ordena el patrón general que comparten la ESP32 y Android.

   Ordena: Consumo · Detección · Estado · Manejador · Suceso

**2.** Sin placa, ¿qué **no** podemos comprobar hoy?

   a) El nivel de tensión real del pin
   b) Que el código de la ISR compila
   c) El rebote mecánico del pulsador
   d) El instante en que el periférico GPIO solicita la interrupción
   e) Qué variable usa la ISR para dejar constancia del evento

### GPIO9, botón BOOT, pull-up y flanco FALLING · Teoría

El programa de la plantilla está escrito para observar un pulsador en el **GPIO9** de la placa de referencia: en la ESP32-C3-DevKitM-1, el botón **BOOT** lleva el GPIO9 a nivel bajo al pulsarlo.

Un **GPIO** (*General-Purpose Input/Output*) es un pin digital configurable como entrada o salida, y puede ser **fuente de interrupciones**.

```cpp
pinMode(PIN_BOTON, INPUT_PULLUP);
attachInterrupt(PIN_BOTON, alPulsarBoton, FALLING);
```

### INPUT_PULLUP: un nivel de reposo conocido

Una entrada «al aire» no tiene un valor fiable. Una **resistencia pull-up** la mantiene en **nivel alto** mientras nadie la toque. `INPUT_PULLUP` activa una resistencia pull-up **interna** del chip, así que no hace falta añadirla por fuera.

> [!ANALOGIA]
> El pull-up es un muelle que mantiene una puerta abierta (nivel alto). Pulsar el botón es empujar la puerta y cerrarla (nivel bajo). Al soltar, el muelle la devuelve a su sitio.

### FALLING: el momento exacto que nos interesa

Al pulsar, el pin pasa de **alto a bajo**: es un **flanco descendente**. Justo eso es lo que pide `FALLING`.

```text
nivel   alto ───────┐           ┌──────── alto
                    │  pulsado  │
        bajo        └───────────┘
                    ↑ FALLING   ↑ (RISING, no nos interesa)
```

Fíjate en la combinación: el pull-up deja el pin **alto en reposo**, y el botón lo baja. Por eso el evento natural «se ha pulsado» es el flanco **descendente**. Si el circuito estuviera al revés (reposo bajo, pulsado alto), el flanco de pulsación sería el ascendente.

> [!EXAMEN]
> Pregunta típica: «¿por qué `FALLING` y no `RISING`?». Respuesta: porque con `INPUT_PULLUP` el reposo es nivel alto y la pulsación lleva el pin a bajo; esa transición alto → bajo es el flanco descendente.

**Preguntas**

**3.** Con `INPUT_PULLUP` y el botón sin pulsar, ¿en qué nivel está el GPIO9?

   a) Alto, gracias a la resistencia pull-up interna
   b) Bajo, porque no hay corriente
   c) Indefinido, oscila entre alto y bajo
   d) Alterna a 115200 baudios

**4.** ¿Qué constante de `attachInterrupt` indica una transición de nivel alto a nivel bajo?

### attachInterrupt y la ISR mínima · Teoría

Este es el código esencial de `src/main.cpp` de la plantilla:

```cpp
constexpr uint8_t PIN_BOTON = 9;
volatile uint32_t eventosHardware = 0;
volatile bool eventoPendiente = false;

void ARDUINO_ISR_ATTR alPulsarBoton() {
  ++eventosHardware;
  eventoPendiente = true;
}

void setup() {
  pinMode(PIN_BOTON, INPUT_PULLUP);
  attachInterrupt(PIN_BOTON, alPulsarBoton, FALLING);
}

void loop() {
  // Copia el estado compartido y procesa el evento fuera de la ISR.
}
```

### Qué hace cada pieza

- **`attachInterrupt(pin, función, modo)`** registra el manejador: «cuando en este pin ocurra este flanco, ejecuta esta función». Es el equivalente de bajo nivel a `setOnClickListener`.
- **Interrupción**: el periférico GPIO solicita atención al procesador, que **desvía temporalmente** la ejecución hacia la rutina asociada y luego vuelve a donde estaba.
- **ISR** (*Interrupt Service Routine*): esa rutina, aquí `alPulsarBoton()`. `ARDUINO_ISR_ATTR` es la marca que Arduino-ESP32 usa para las funciones de interrupción.

### La regla de oro de la ISR: lo mínimo imprescindible

Una ISR puede ejecutarse en **cualquier momento** respecto al flujo normal. Por eso no conviene meter en ella:

- operaciones lentas o esperas (`delay`),
- lógica de aplicación compleja,
- accesos que dependan de bloqueos.

La de la plantilla solo hace dos cosas: **incrementa un contador** y **marca un indicador**. Detectar y registrar; nada más.

> [!ANALOGIA]
> La ISR es el conserje que oye el timbre: apunta «ha llamado alguien» en una libreta y vuelve a su puesto. No sube a atender la visita; eso lo hará quien lea la libreta (`loop()`).

**Preguntas**

**5.** ¿Qué línea **registra** el manejador del evento?

   a) `pinMode(PIN_BOTON, INPUT_PULLUP);`
   b) `volatile bool eventoPendiente = false;`
   c) `++eventosHardware;`
   d) `attachInterrupt(PIN_BOTON, alPulsarBoton, FALLING);`

**6.** ¿Qué **no** debería hacer una ISR?

   a) Esperar con `delay(500)`
   b) Incrementar un contador
   c) Ejecutar la lógica completa de la aplicación
   d) Marcar un indicador booleano
   e) Esperar a que se libere un bloqueo

### Ejercicio: recorre el evento etapa a etapa · Ejercicio

Juntemos todo en una tabla. Cada fila es una etapa del recorrido y, a la derecha, **dónde la vemos hoy**, sin placa.

| Etapa | Qué ocurre | Dónde lo vemos hoy |
| --- | --- | --- |
| 1. Señal física | La tensión del GPIO pasa de alto a bajo | Solo la razonamos |
| 2. GPIO | La lógica de entrada detecta el nivel y el flanco configurado | `pinMode` y `FALLING` |
| 3. Interrupción | El periférico solicita atención al procesador | `attachInterrupt` registra el manejador |
| 4. ISR | El procesador desvía la ejecución a `alPulsarBoton()` | Podemos leer y compilar la función |
| 5. Estado | La ISR deja constancia para el resto del programa | `eventoPendiente` y `eventosHardware` |
| 6. Consumo | `loop()` procesa el evento en contexto normal | Seguimos la lógica y verificamos que compila |

![Arriba, la forma de onda del GPIO9 con INPUT_PULLUP: en reposo está en alto gracias al pull-up, al pulsar BOOT baja (ese flanco descendente es FALLING) y al soltar vuelve a subir (RISING, que no nos interesa). Debajo, el recorrido en dos filas. Hardware: 1 Señal, la tensión pasa a bajo; 2 GPIO, detecta el flanco; 3 Interrupción, avisa al procesador. Software: 4 ISR alPulsarBoton(), detecta y registra; 5 Estado, eventoPendiente y eventosHardware; 6 loop(), interpreta y actúa en contexto normal.](/img/pde/tema-0/recorrido-evento.svg "El recorrido del evento: del flanco FALLING en el pin hasta loop()")

> [!EXAMEN]
> El recorrido **GPIO → interrupción → ISR → estado compartido → loop()** aparece literalmente en los objetivos de la unidad. Tienes que saber ordenarlo y señalar en el código qué línea corresponde a cada etapa.

Haz el ejercicio sin mirar la tabla: tápala con la mano.

**Preguntas**

**7.** Ordena el recorrido del evento desde el botón hasta tu código.

   Ordena: Consumo: `loop()` procesa el evento · Estado: se actualizan `eventoPendiente` y `eventosHardware` · GPIO: la lógica de entrada detecta el flanco FALLING · ISR: se ejecuta `alPulsarBoton()` · Interrupción: el periférico pide atención al procesador · Señal física: la tensión del pin baja

**8.** ¿En qué etapa del recorrido encaja la línea `eventoPendiente = true;`?

   a) GPIO
   b) Interrupción
   c) Estado
   d) Consumo

### volatile, estado compartido y antirrebote · Teoría

### ¿Por qué `volatile`?

`eventosHardware` y `eventoPendiente` se modifican **desde la ISR** y se leen **desde `loop()`**. Para el compilador, `loop()` es un bucle normal y podría «optimizar» la lectura guardando el valor en un registro y no volviendo a mirar la memoria. `volatile` le dice: *esta variable puede cambiar por fuera del flujo que ves; léela de memoria cada vez*.

### Detectar y registrar ≠ interpretar y actuar

La plantilla separa dos responsabilidades:

<div class="cards">
  <div class="card"><h4>ISR</h4><p>Detecta y registra. Brevísima. Contador + indicador.</p></div>
  <div class="card"><h4>loop()</h4><p>Copia el estado compartido e interpreta y actúa en contexto normal, donde sí puede tardar.</p></div>
</div>

Esta separación volverá con temporizadores, comunicaciones, sensores y otros periféricos.

### Antirrebote (*debounce*)

Un pulsador mecánico no pasa limpiamente de alto a bajo: sus contactos **rebotan** y pueden producir **varios flancos en una sola pulsación**. Cada uno dispara la ISR. Por eso el ejemplo añade un **antirrebote temporal** en el consumo: se ignoran los avisos que llegan demasiado pronto tras el último aceptado.

Un boceto orientativo de cómo puede quedar `loop()` (la plantilla trae su propia versión):

```cpp
void loop() {
  static uint32_t ultimaAceptada = 0;
  if (eventoPendiente) {
    eventoPendiente = false;               // consumimos el aviso
    uint32_t total = eventosHardware;      // copia del estado compartido
    uint32_t ahora = millis();
    if (ahora - ultimaAceptada >= 50) {    // antirrebote: p. ej. 50 ms
      ultimaAceptada = ahora;
      Serial.printf("Pulsación aceptada (flancos vistos por la ISR: %lu)\n",
                    (unsigned long) total);
    }
  }
}
```

Observa que el contador `eventosHardware` puede crecer más que las pulsaciones aceptadas: la ISR cuenta **flancos**, `loop()` decide cuáles son **pulsaciones**.

![Línea temporal de una pulsación con rebote. Arriba, el GPIO9 baja tres veces seguidas: tres flancos FALLING. En cada flanco se ejecuta una ISR brevísima que suma uno, y eventosHardware pasa de 1 a 2 y a 3. Debajo, loop() queda partido en trozos porque la CPU lo desvía a la ISR en cada flanco y luego vuelve. Al final, loop() lee el aviso, interpreta y actúa: total igual a 3 flancos, 1 pulsación.](/img/pde/tema-0/isr-vs-loop.svg "La ISR detecta y registra cada flanco; loop() interpreta y decide cuántas pulsaciones hubo")

**Preguntas**

**9.** ¿Por qué `eventoPendiente` se declara `volatile`?

   a) Para que se guarde en la flash y sobreviva a un reinicio
   b) Porque cambia desde la ISR y el compilador no debe suponer que su valor sigue igual entre lecturas
   c) Para que la ISR se ejecute más rápido
   d) Porque es obligatorio en todas las variables globales de Arduino

**10.** Pulsas BOOT **una** vez y `eventosHardware` pasa de 0 a 3. ¿Qué ha ocurrido?

   a) La ISR tiene un bucle que suma tres veces
   b) El pull-up interno está mal configurado
   c) `FALLING` dispara en los dos flancos y además en el nivel
   d) El rebote mecánico ha producido varios flancos y cada uno ha disparado la ISR

### Ejercicio: identifica las piezas en main.cpp · Ejercicio

La guía propone este ejercicio para hacer **sin placa**: localiza en `src/main.cpp` cada pieza del evento y compáralo después con un `onClick`, donde las capas inferiores ya están ocultas.

```cpp
constexpr uint8_t PIN_BOTON = 9;                       // (A)
volatile uint32_t eventosHardware = 0;                 // (B)
volatile bool eventoPendiente = false;                 // (B)

void ARDUINO_ISR_ATTR alPulsarBoton() {                // (C)
  ++eventosHardware;
  eventoPendiente = true;
}

void setup() {
  pinMode(PIN_BOTON, INPUT_PULLUP);                    // (D)
  attachInterrupt(PIN_BOTON, alPulsarBoton, FALLING);  // (E)
}

void loop() {                                          // (F)
  // Copia el estado compartido y procesa el evento fuera de la ISR.
}
```

Piensa en el equivalente Android de cada letra:

```kotlin
boton.setOnClickListener {      // registro del manejador
    pulsaciones++               // manejador que cambia el estado
    actualizarVista()           // consumo: refleja el estado en la UI
}
```

<details><summary>Ver solución completa</summary>
<p>(A) identifica el origen; (D) configura el origen (entrada con pull-up); (E) registra el manejador y el tipo de evento; (C) es la ISR; (B) es el estado compartido; (F) es el consumo. En Android, el «origen» (el táctil, la vista) y la detección ya vienen resueltos por el sistema: solo registras y escribes el manejador.</p>
</details>

**Preguntas**

**11.** ¿Qué papel juega la línea (D), `pinMode(PIN_BOTON, INPUT_PULLUP);`?

   a) Registra la ISR
   b) Configura el origen del evento: el pin como entrada con pull-up
   c) Consume el evento
   d) Declara el estado compartido

**12.** ¿Qué línea de la ESP32 hace el papel de `boton.setOnClickListener { ... }`?

   a) (A) `constexpr uint8_t PIN_BOTON = 9;`
   b) (F) `void loop()`
   c) (E) `attachInterrupt(PIN_BOTON, alPulsarBoton, FALLING);`
   d) (B) `volatile bool eventoPendiente = false;`

### Repaso: glosario y preguntas de examen · Repaso

> [!RESUMEN] Del pin al loop()
> El botón BOOT baja el **GPIO9**, que en reposo está alto por `INPUT_PULLUP`. Esa transición es un flanco **FALLING**. `attachInterrupt` registra la **ISR** `alPulsarBoton()`, que solo incrementa un contador y marca un indicador `volatile`. `loop()` copia ese estado, aplica **antirrebote** y actúa. **La ISR detecta y registra; el resto del programa interpreta.**

| Término | Definición breve |
| --- | --- |
| **ESP32-C3** | Microcontrolador de Espressif con CPU RISC-V de 32 bits, Wi-Fi y Bluetooth Low Energy |
| **RISC-V** | Arquitectura abierta de conjunto de instrucciones |
| **Toolchain** | Compilador, ensamblador, enlazador y utilidades que generan firmware para una arquitectura |
| **Firmware** | Programa compilado para ejecutarse en un sistema empotrado |
| **Build** | Transformar el código fuente en firmware (compilación + enlazado) |
| **Artefacto** | Fichero generado por la construcción: `firmware.elf`, `firmware.bin` |
| **Board** | Identificador de una placa concreta en PlatformIO |
| **Arduino-ESP32** | API de estilo Arduino sobre los ESP32; se descarga con `framework = arduino` |
| **GPIO** | Pin digital de entrada/salida, posible fuente de interrupciones |
| **Pull-up** | Resistencia que mantiene una entrada en nivel alto |
| **FALLING** | Disparo en transición de alto a bajo |
| **Interrupción** | Un periférico desvía temporalmente la ejecución a una rutina |
| **ISR** | Rutina de interrupción; corta y sin trabajo bloqueante |
| **Debounce** | Filtrar las transiciones rápidas de un pulsador mecánico |
| **Serial Monitor** | Leer y enviar datos por puerto serie (con placa conectada) |
| **Git Bash** | Terminal de Git for Windows con entorno tipo Unix |

**Preguntas**

**13.** ¿Cómo se llama el conjunto de compilador, ensamblador, enlazador y utilidades que transforma el código fuente en firmware para una arquitectura concreta?

**14.** ¿Cuál es la definición correcta de ISR?

   a) Rutina que se ejecuta al desviar el procesador la ejecución por una interrupción; debe ser corta y no bloqueante
   b) Bucle principal del firmware que se repite indefinidamente
   c) Resistencia interna que fija el nivel de un pin
   d) Herramienta para leer el puerto serie

**15.** Marca las afirmaciones **correctas**.

   a) El núcleo de la ESP32-C3 implementa una variante RISC-V de 32 bits
   b) `INPUT_PULLUP` deja el pin en nivel bajo en reposo
   c) `firmware.elf` y `firmware.bin` son artefactos de compilación
   d) El antirrebote se hace dentro de la ISR con `delay`
   e) `board` identifica una placa concreta dentro de PlatformIO

**16.** Pregunta de examen: ¿qué reparto de trabajo propone la plantilla entre la ISR y `loop()`?

   a) La ISR interpreta y actúa; `loop()` solo duerme
   b) Ambas hacen lo mismo para tener redundancia
   c) La ISR detecta y registra; `loop()` copia el estado, interpreta y actúa
   d) `loop()` detecta el flanco por sondeo y la ISR imprime por serie

### Solucionario

1. 1. Suceso → 2. Detección → 3. Manejador → 4. Estado → 5. Consumo  
   Suceso → detección → manejador → estado → consumo. En Android las primeras etapas las hacen capas que no ves; en la ESP32 las configuras tú.
2. a) El nivel de tensión real del pin; c) El rebote mecánico del pulsador; d) El instante en que el periférico GPIO solicita la interrupción  
   Lo eléctrico y temporal (tensión, rebote, instante de la petición) necesita hardware. Leer y compilar el código, incluida la ISR y sus variables, sí se puede hacer hoy.
3. a) Alto, gracias a la resistencia pull-up interna  
   La pull-up interna mantiene la entrada en nivel alto en reposo. Al pulsar BOOT, el pin baja.
4. `FALLING`  
   `FALLING` es la condición de disparo para el flanco descendente (alto → bajo).
5. d) `attachInterrupt(PIN_BOTON, alPulsarBoton, FALLING);`  
   `attachInterrupt` asocia el pin, la función `alPulsarBoton` y el modo `FALLING`. `pinMode` configura el origen, no registra nada.
6. a) Esperar con `delay(500)`; c) Ejecutar la lógica completa de la aplicación; e) Esperar a que se libere un bloqueo  
   La ISR debe ser brevísima: registrar (contador, indicador) y salir. Esperas, lógica compleja y bloqueos van fuera, en el contexto normal.
7. 1. Señal física: la tensión del pin baja → 2. GPIO: la lógica de entrada detecta el flanco FALLING → 3. Interrupción: el periférico pide atención al procesador → 4. ISR: se ejecuta `alPulsarBoton()` → 5. Estado: se actualizan `eventoPendiente` y `eventosHardware` → 6. Consumo: `loop()` procesa el evento  
   Señal → GPIO → interrupción → ISR → estado → consumo. La ISR solo llega hasta el estado; el consumo ocurre después, en contexto normal.
8. c) Estado  
   Se ejecuta dentro de la ISR, pero su efecto es dejar constancia del suceso: es la etapa de **estado compartido** que luego leerá `loop()`.
9. b) Porque cambia desde la ISR y el compilador no debe suponer que su valor sigue igual entre lecturas  
   La escribe la ISR y la lee `loop()`. `volatile` obliga a leerla de memoria cada vez, en lugar de reutilizar un valor que el compilador creía constante.
10. d) El rebote mecánico ha producido varios flancos y cada uno ha disparado la ISR  
   Los contactos rebotan y generan varios flancos descendentes. Por eso el consumo en `loop()` aplica un antirrebote temporal.
11. b) Configura el origen del evento: el pin como entrada con pull-up  
   `pinMode` prepara el origen: el GPIO9 como entrada con resistencia pull-up interna. El registro lo hace (E).
12. c) (E) `attachInterrupt(PIN_BOTON, alPulsarBoton, FALLING);`  
   Ambas registran un manejador para un tipo de evento de una fuente. La diferencia es el nivel: `attachInterrupt` trabaja directamente con el pin y la interrupción.
13. `toolchain`  
   Es la **toolchain**. PlatformIO descarga la de RISC-V automáticamente.
14. a) Rutina que se ejecuta al desviar el procesador la ejecución por una interrupción; debe ser corta y no bloqueante  
   ISR = *Interrupt Service Routine*. El bucle principal es `loop()`, la resistencia es el pull-up y la herramienta es el Serial Monitor.
15. a) El núcleo de la ESP32-C3 implementa una variante RISC-V de 32 bits; c) `firmware.elf` y `firmware.bin` son artefactos de compilación; e) `board` identifica una placa concreta dentro de PlatformIO  
   `INPUT_PULLUP` deja el pin **alto** en reposo, y la ISR nunca debe esperar con `delay`: el antirrebote se aplica al consumir el evento en `loop()`.
16. c) La ISR detecta y registra; `loop()` copia el estado, interpreta y actúa  
   Separar «detectar y registrar» (ISR) de «interpretar y actuar» (contexto normal) mantiene la ISR mínima y es la idea que se repetirá con temporizadores y comunicaciones.

---

# Tema 1 · Fundamentos y origen de los eventos

Qué significa que un programa esté dirigido por eventos: quién controla el siguiente paso, inversión de control, el vocabulario fuente / evento / receptor y el viaje de un suceso desde el mundo físico (polling, interrupciones, IRQ, ISR, driver, latencia) hasta tu código. Lo aplicamos reescribiendo agenda.kt como agendaeventos.kt y clasificando diez situaciones reales.

## El paradigma dirigido por eventos

_Flujo secuencial frente a flujo dirigido por eventos, inversión de control, el vocabulario fuente / evento / receptor y el viaje de un suceso desde el mundo físico hasta tu código: polling, interrupciones, IRQ, ISR, driver y latencia._

### ¿Quién controla el siguiente paso? · Teoría

Toda la asignatura gira alrededor de una pregunta muy sencilla: **¿quién decide cuál es el siguiente paso del programa?**

En los programas que habéis escrito hasta ahora la respuesta era obvia: el propio programa. Ejecuta la instrucción 1, luego la 2, luego la 3. Si lees el código de arriba abajo, sabes en qué orden va a pasar todo.

En un programa **dirigido por eventos** esa respuesta cambia: el **entorno** determina *cuándo* llega un suceso (un clic, un mensaje, un temporizador que vence, un sensor que se dispara) y la aplicación se limita a **reaccionar**.

| | Programa secuencial | Programa dirigido por eventos |
| --- | --- | --- |
| Quién decide el orden | El programa: 1 → 2 → 3 | El entorno decide cuándo llega cada suceso |
| Qué hace el código | Sigue su guion de principio a fin | Reacciona a lo que va ocurriendo |
| Orden de ejecución | Fijo, se lee en el código | Depende de cuándo llegan los eventos |

![Arriba, un programa secuencial: instrucción 1, instrucción 2, instrucción 3 y fin, en un orden fijo que decide el programa. Abajo, un programa dirigido por eventos: en la franja del entorno llegan un clic, un mensaje, un temporizador y un sensor en momentos irregulares de una línea de tiempo, y cada uno dispara una reacción en la franja de la aplicación.](/img/pde/tema-1/secuencial-vs-eventos.svg "Secuencial: el programa fija el orden. Dirigido por eventos: el entorno decide cuándo llega cada suceso")

> [!ANALOGIA] Receta frente a recepción de hotel
> Un cocinero con una receta decide el orden: pelar, cortar, freír. Un recepcionista no puede: no sabe si lo siguiente será una llamada, un cliente con una queja o un repartidor. Tiene preparada una respuesta para cada caso y la ejecuta **cuando el suceso llega**.

La Programación Orientada / Dirigida por Eventos (POE) es un **paradigma general**: el software reacciona a sucesos. Al terminar la unidad deberías poder:

- **Comprender** que la POE no es "cosa de ventanas", sino una forma general de organizar software que reacciona.
- **Distinguir** flujo secuencial y flujo dirigido por eventos.
- **Explicar** la cadena *estímulo externo → evento software → reacción*.
- **Reconocer** el paradigma en GUI, móviles, servidores, IoT y firmware.

> [!EXAMEN]
> En POE el orden de las reacciones depende del **momento en que llegan los eventos**, no del orden en el que escribiste los manejadores en el fichero.

**Preguntas**

**1.** En un programa dirigido por eventos, ¿qué determina el orden en que se ejecutan las reacciones?

   a) El orden en que están escritas las funciones en el fichero
   b) El momento en que llegan los eventos
   c) El orden alfabético de los nombres de los manejadores
   d) La prioridad que el compilador asigna a cada función

**2.** ¿Cuáles de estos programas siguen el paradigma dirigido por eventos?

   a) Una app móvil que reacciona a los toques en la pantalla
   b) Un script que lee un CSV, calcula la media y termina
   c) Un servidor web que atiende peticiones según van llegando
   d) El firmware de un termostato que reacciona a la interrupción de un sensor

### POO organiza piezas; POE organiza reacciones · Teoría

No confundas la POE con una alternativa a la POO. Responden a preguntas distintas:

<div class="cards">
  <div class="card"><h4>POO</h4><p>Organiza <b>piezas</b>: objetos, estado y métodos.</p></div>
  <div class="card"><h4>POE</h4><p>Organiza <b>reacciones</b>: sucesos, fuentes, receptores y reacciones.</p></div>
  <div class="card"><h4>Lo que añade la POE</h4><p>Qué ocurre, quién lo comunica y <b>cuándo</b> se ejecuta cada respuesta.</p></div>
</div>

En esta asignatura escribiréis POE **con** POO (Kotlin): los receptores serán objetos, lambdas o clases que implementan una interfaz.

## Inversión de control (IoC)

La idea central del paradigma tiene nombre propio: **inversión de control**.

- La aplicación **no inicia cada llamada** ni decide el orden completo.
- Es el **framework, el sistema operativo, una biblioteca o el hardware** quien invoca la reacción que tú registraste.
- Se resume en el **principio de Hollywood**: *«No nos llames; nosotros te llamaremos»*.

```kotlin
// Flujo convencional: MI código llama a la biblioteca, y yo decido cuándo
val texto = readLine()
procesar(texto)

// Inversión de control: entrego una reacción y OTRO decide cuándo llamarla
boton.setOnClickListener {
    procesar("clic")   // lo invocará el framework cuando el usuario pulse
}
```

Fíjate en la diferencia: en el primer bloque, `procesar` se ejecuta justo después de `readLine()`. En el segundo, al llegar a esa línea **no se ejecuta nada**: solo se *registra* el código. Se ejecutará más tarde, cero, una o cien veces, según lo que haga el usuario.

![Dos paneles. Flujo convencional: mi código llama a la biblioteca (readLine) y la biblioteca le devuelve el dato; mi código decide cuándo. Inversión de control: primero mi código registra una reacción (setOnClickListener) en el framework, el SO o el hardware; después es ese entorno el que, cuando ocurre el suceso, llama a mi reacción. Debajo, el principio de Hollywood: «No nos llames; nosotros te llamaremos».](/img/pde/tema-1/inversion-de-control.svg "Flujo convencional frente a inversión de control: cambia quién llama a quién")

> [!EXAMEN] La frase que hay que saber decir
> Flujo convencional: **mi código llama a la biblioteca**. Inversión de control: **el entorno llama a mi código**.

**Preguntas**

**3.** ¿Cómo se llama el principio que resume la inversión de control con la frase «No nos llames; nosotros te llamaremos»?

**4.** ¿Qué describe mejor la inversión de control?

   a) El programa invierte el orden de sus instrucciones para ir más rápido
   b) La POE sustituye a la POO, que deja de ser necesaria
   c) El entorno (framework, SO, biblioteca o hardware) invoca la reacción que la aplicación registró
   d) La aplicación llama a la biblioteca cada vez que necesita un dato

### Ejercicio: ¿hay inversión de control? · Ejercicio

Mira estos cinco fragmentos (Kotlin/Android y Arduino) y decide en cuáles **es otro quien decide cuándo se ejecuta tu código**.

```kotlin
// A
val n = readLine()!!.toInt()
println(n * 2)

// B
boton.setOnClickListener { contador++ }

// C
Handler(Looper.getMainLooper()).postDelayed({ mostrarAviso() }, 5000)

// D
val total = listOf(3, 4, 5).sum()
```

```cpp
// E (Arduino / ESP32)
attachInterrupt(digitalPinToInterrupt(PIN_BOTON), alPulsar, FALLING);
```

Pista para razonar cada uno: pregúntate **quién llama a la función** y **quién decide el momento**.

<details><summary>Ver razonamiento</summary>
<p><b>A</b>: tu código llama a <code>readLine()</code> y se queda esperando; cuando vuelve, sigue en el orden que tú escribiste. Flujo convencional.</p>
<p><b>B</b>: registras una lambda; el framework la invoca cuando el usuario pulsa. IoC (botón).</p>
<p><b>C</b>: entregas <code>mostrarAviso()</code>; el sistema la activa al vencer el tiempo. IoC (temporizador).</p>
<p><b>D</b>: llamas a <code>sum()</code> y obtienes el resultado al momento. Convencional.</p>
<p><b>E</b>: registras <code>alPulsar</code>; el hardware, a través de una interrupción, decide cuándo se ejecuta. IoC (sensor / pin).</p>
</details>

> [!TRUCO]
> Si en la línea donde aparece tu función **no se ejecuta nada todavía** (solo se pasa como parámetro para "más tarde"), casi seguro estás ante inversión de control.

**Preguntas**

**5.** ¿Qué fragmentos muestran inversión de control?

   a) A
   b) B
   c) C
   d) D
   e) E

**6.** En el fragmento C, ¿quién decide el momento exacto en que se ejecuta `mostrarAviso()`?

   a) La línea siguiente del código, que la llama explícitamente
   b) El sistema, al vencer el plazo de 5000 ms
   c) El usuario, al pulsar un botón
   d) Nadie: se ejecuta en cuanto se lee esa línea

### Vocabulario esencial: evento, fuente, receptor… · Teoría

Para hablar con precisión de eventos necesitamos cinco palabras. Apréndelas bien: aparecerán en todas las unidades.

| Término | Definición |
| --- | --- |
| **Evento** | Suceso relevante que el software puede comunicar y procesar. |
| **Fuente / emisor** | Componente, dispositivo o sistema que **origina o publica** el evento. |
| **Receptor** | Componente **interesado** en recibir la notificación. |
| **Notificación** | Comunicación que **entrega** el evento al receptor. |
| **Reacción** | Trabajo que realiza el receptor al **procesar** el evento. |

<div class="flow">
  <div><b>Fuente</b><small>origina el suceso</small></div>
  <div><b>Evento</b><small>qué pasa + datos</small></div>
  <div><b>Notificación</b><small>lo entrega</small></div>
  <div><b>Receptor</b><small>reacciona</small></div>
</div>

## Todo evento lleva información

Un evento no es solo "algo ha pasado". Lleva cuatro datos:

| Campo | Pregunta | Ejemplos |
| --- | --- | --- |
| **Tipo** | ¿Qué ocurrió? | clic, timeout, mensaje, temperatura alta |
| **Origen** | ¿Quién lo produjo? | botón, sensor, conexión, tarea |
| **Datos** | ¿Con qué información? | tecla, valor, coordenadas, contenido |
| **Instante** | ¿Cuándo? | marca de tiempo; ayuda a razonar sobre **orden y latencia** |

En código podría representarse así:

```kotlin
data class Evento(
    val tipo: String,     // "clic", "timeout", "temperatura_alta"
    val origen: String,   // "botonEnviar", "sensorNevera"
    val datos: Any?,      // tecla, valor leído, coordenadas…
    val instante: Long    // milisegundos desde el arranque, por ejemplo
)
```

> [!EXAMEN] Fórmula para memorizar
> **Evento = tipo + origen + datos + instante.** La fuente produce o detecta, el receptor procesa y la notificación los conecta.

**Preguntas**

**7.** ¿Qué información lleva todo evento, según el modelo de la unidad?

   a) Tipo
   b) Receptor
   c) Origen
   d) Datos
   e) Prioridad
   f) Instante

**8.** ¿Cómo se llama el componente **interesado** en recibir la notificación de un evento?

### Ejercicio: pon nombre a cada pieza · Ejercicio

Escenario: estás en un editor de texto con el cuadro de búsqueda abierto. **Pulsas la tecla Esc** y el cuadro se cierra.

Desmonta la escena con el vocabulario del paso anterior antes de mirar la solución:

| Pieza | Tu respuesta |
| --- | --- |
| Fuente | ¿? |
| Evento (tipo, origen, datos, instante) | ¿? |
| Notificación | ¿? |
| Receptor | ¿? |
| Reacción | ¿? |

<details><summary>Ver una solución</summary>
<p><b>Fuente</b>: el usuario a través del teclado (y el sistema que capta la pulsación).</p>
<p><b>Evento</b>: tipo «tecla pulsada»; origen: el teclado; datos: el código de la tecla Esc; instante: la marca de tiempo de la pulsación.</p>
<p><b>Notificación</b>: el sistema / framework entrega ese evento al componente que tiene el foco.</p>
<p><b>Receptor</b>: el manejador de teclado del cuadro de búsqueda.</p>
<p><b>Reacción</b>: cerrar el cuadro de búsqueda.</p>
</details>

> [!CUIDADO] Error típico
> Decir que la fuente es "la tecla Esc" y que el evento también es "la tecla Esc". Si dos columnas te salen iguales, algo falla: la **fuente** es quien origina (usuario + teclado); el **evento** es lo que ha pasado, con sus datos (pulsación de Esc).

**Preguntas**

**9.** En el escenario de la tecla Esc, ¿cuál es la **reacción**?

   a) La pulsación de la tecla
   b) El teclado
   c) Entregar el evento al cuadro de búsqueda
   d) Cerrar el cuadro de búsqueda

**10.** ¿Qué campo del evento permite distinguir esta pulsación de una pulsación de la tecla Intro?

   a) El instante
   b) Los datos (el código de la tecla)
   c) El origen
   d) El receptor

**11.** ¿Qué campo del evento ayuda a razonar sobre el **orden** y la **latencia**?

### Del mundo físico al software: polling o interrupción · Teoría

Un clic no aparece por arte de magia en tu `onClick`. Recorre cinco etapas:

<div class="flow">
  <div><b>1 · Estímulo</b><small>usuario, sensor, red, temporizador, dispositivo</small></div>
  <div><b>2 · Detección</b><small>hardware: polling o interrupción</small></div>
  <div><b>3 · Interpretación</b><small>driver/controlador y SO</small></div>
  <div><b>4 · Evento software</b><small>tipo, origen y datos</small></div>
  <div><b>5 · Reacción</b><small>notificación a la app y respuesta</small></div>
</div>

La etapa 2 merece atención: hay **dos formas** de detectar que algo ha ocurrido.

| | Polling | Interrupción |
| --- | --- | --- |
| Idea | Consulta repetida: «¿ha ocurrido algo?» | Una señal pide atención a la CPU cuando ocurre el suceso |
| Ventaja | Muy simple | La CPU puede hacer otro trabajo hasta recibirla |
| Inconveniente | Consume CPU y la respuesta depende de la frecuencia de consulta | Más compleja de configurar |

> [!ANALOGIA] El móvil en silencio
> **Polling**: miras el móvil cada cinco minutos para ver si te ha escrito alguien. Si el mensaje llega justo después de mirar, no te enteras hasta la siguiente vez. **Interrupción**: el móvil vibra y te avisa; mientras tanto puedes estudiar tranquilamente.

> [!IMPORTANTE]
> Si se consulta tarde, el evento se detecta tarde: con polling **aumenta la latencia de detección**.

![Dos líneas de tiempo. Polling: la CPU consulta a intervalos regulares si ha ocurrido algo; el suceso llega justo después de una consulta y no se detecta hasta la siguiente, y ese hueco es la latencia de detección. Interrupción: la CPU hace otro trabajo; cuando ocurre el suceso, la señal IRQ pide atención, la CPU ejecuta la ISR, interviene el driver y después la CPU sigue con lo suyo.](/img/pde/tema-1/polling-vs-interrupcion.svg "Polling frente a interrupción: con polling el suceso espera a la siguiente consulta")

```cpp
// Polling: el bucle pregunta una y otra vez
void loop() {
  if (digitalRead(PIN_BOTON) == LOW) {   // ¿ha pasado algo?
    encenderLed();
  }
  delay(100);                             // vuelve a preguntar dentro de 100 ms
}
```

**Preguntas**

**12.** Ordena las etapas del recorrido de un suceso, del mundo físico a la aplicación.

   Ordena: Detección · Estímulo · Evento software · Interpretación (driver y SO) · Reacción

**13.** ¿Cuál es el principal inconveniente del polling?

   a) Necesita un driver especial que no existe en microcontroladores
   b) Consume CPU y su tiempo de respuesta depende de la frecuencia de consulta
   c) Solo funciona con eventos de red
   d) La CPU no puede hacer nada más hasta que llega la interrupción

### IRQ, ISR y driver; latencia · Teoría

Tres siglas que en el examen **no** se pueden mezclar:

<div class="cards">
  <div class="card"><h4>IRQ</h4><p><i>Interrupt Request</i>: la <b>solicitud o señal</b> de interrupción que genera un dispositivo o controlador.</p></div>
  <div class="card"><h4>ISR</h4><p><i>Interrupt Service Routine</i>: el <b>código</b> que ejecuta la CPU para atender la interrupción.</p></div>
  <div class="card"><h4>Driver / controlador</h4><p><b>Software que traduce</b> los detalles del dispositivo a operaciones y datos utilizables por el sistema.</p></div>
</div>

> [!ANALOGIA] El timbre de casa
> La **IRQ** es el timbre que suena. La **ISR** es lo que haces al oírlo: dejar lo que estabas haciendo e ir a abrir. El **driver** sería quien traduce al visitante: "es el repartidor, trae un paquete para ti".

## Latencia: el tiempo hasta poder reaccionar

**Latencia** = tiempo desde que ocurre el estímulo hasta que la aplicación **puede** reaccionar.

No se genera en un único sitio: se **acumula** a lo largo de todo el recorrido.

<div class="flow">
  <div><b>Detección</b><small>polling o interrupción</small></div>
  <div><b>Driver</b><small>interpretación</small></div>
  <div><b>Transferencia</b><small>mover los datos</small></div>
  <div><b>Generación</b><small>crear el evento</small></div>
  <div><b>Entrega</b><small>notificar</small></div>
  <div><b>Reacción</b><small>tu código</small></div>
</div>

La capacidad de respuesta depende de **todo el recorrido**, no solo del código final. Optimizar tu `onClick` no sirve de nada si el retraso está en un polling lento.

![Las cinco etapas del recorrido de un suceso, de arriba abajo, con la capa en la que ocurre cada una: estímulo (mundo físico), detección por polling o interrupción con su IRQ (hardware), interpretación con la ISR y el driver y evento software con tipo, origen y datos (driver y SO), y reacción con la notificación a la app y tu código (aplicación). A la derecha, una línea naranja muestra cómo la latencia se va sumando desde t = 0: detección, driver y transferencia, generación del evento, entrega y reacción.](/img/pde/tema-1/recorrido-capas-latencia.svg "Cada capa del recorrido añade su parte de latencia")

> [!EXAMEN] Pregunta clave
> Ante un sistema que "va lento reaccionando": **¿en qué capa se está añadiendo tiempo?**

**Preguntas**

**14.** ¿Qué es una ISR?

   a) La señal eléctrica que un dispositivo envía para pedir atención
   b) El código que ejecuta la CPU para atender una interrupción
   c) El software que traduce los detalles de un dispositivo para el sistema operativo
   d) El tiempo que pasa desde el estímulo hasta la reacción

**15.** ¿Dónde puede acumularse latencia en el recorrido de un evento?

   a) En la detección (polling o interrupción)
   b) En el driver
   c) En la entrega del evento a la aplicación
   d) Únicamente en el código final de la aplicación
   e) En la propia reacción

### Ejercicio: ¿dónde se pierde el tiempo? · Ejercicio

Un ESP32 lee un pulsador con el `loop()` del paso 6: comprueba el pin y luego hace `delay(100)`.

```cpp
void loop() {
  if (digitalRead(PIN_BOTON) == LOW) encenderLed();
  delay(100);
}
```

Razona estas situaciones:

1. El usuario pulsa **justo después** de que el programa haya comprobado el pin. ¿Cuánto puede tardar, como mucho, en detectarse la pulsación (despreciando el tiempo de `digitalRead`)?
2. Si en vez de `delay(100)` pusiéramos `delay(10)`, ¿qué ganamos y qué perdemos?
3. Si registramos el pin con `attachInterrupt(...)`, ¿qué pasa con esa espera?

<details><summary>Ver razonamiento</summary>
<p>1. Hasta la siguiente consulta: casi <b>100 ms</b> de latencia de detección en el peor caso.</p>
<p>2. Detectamos antes (≈10 ms en el peor caso), pero la CPU pasa mucho más tiempo preguntando: más consumo para nada la mayoría de las veces.</p>
<p>3. La señal de interrupción (IRQ) avisa en cuanto ocurre el cambio y la CPU ejecuta la ISR: la latencia de detección deja de depender de la frecuencia de consulta, y la CPU puede hacer otro trabajo mientras tanto.</p>
</details>

> [!NOTA]
> Volveréis a este dilema en el laboratorio con el ESP32: es la misma decisión que toma cualquier firmware que tiene que reaccionar a un botón o a un sensor.

**Preguntas**

**16.** Con polling cada 100 ms, ¿cuántos milisegundos puede tardar como mucho en detectarse la pulsación? (escribe solo el número)

**17.** ¿Qué sigla designa la **señal** con la que el controlador del pin pide atención a la CPU?

### Ampliación: la POO como soporte de la POE · Teoría

> [!NOTA] Ampliación (contenido *stretch*)
> Este paso y el siguiente van más allá de lo obligatorio. Merece la pena leerlos: preparan la Unidad 2 (listeners y Observer).

La POO no sustituye al modelo de eventos: **lo hace extensible y desacoplado**. Tres mecanismos lo consiguen:

| Mecanismo | Qué aporta a la POE |
| --- | --- |
| **Interfaz** | Expresa el **contrato** de una reacción sin fijar la implementación. Una fuente puede notificar a receptores distintos mediante ese contrato común. |
| **Polimorfismo** | Distintos receptores comparten contrato y **reaccionan de forma diferente** a la misma notificación. |
| **Composición** | Objetos que colaboran para construir fuentes, receptores y cadenas de reacciones, **sin concentrar toda la lógica en una clase**. |

```kotlin
// Interfaz: el contrato de la reacción
interface ReceptorTemperatura {
    fun onTemperatura(grados: Double)
}

// Polimorfismo: mismo contrato, reacciones distintas
class Pantalla : ReceptorTemperatura {
    override fun onTemperatura(grados: Double) = println("Ahora: $grados °C")
}

class Alarma(private val umbral: Double) : ReceptorTemperatura {
    override fun onTemperatura(grados: Double) {
        if (grados > umbral) println("¡ALERTA! $grados °C")
    }
}

// Composición: la fuente guarda una lista de receptores
class Sensor {
    private val receptores = mutableListOf<ReceptorTemperatura>()
    fun registrar(r: ReceptorTemperatura) { receptores.add(r) }
    fun nuevaLectura(grados: Double) = receptores.forEach { it.onTemperatura(grados) }
}

fun main() {
    val sensor = Sensor()
    sensor.registrar(Pantalla())
    sensor.registrar(Alarma(25.0))
    sensor.nuevaLectura(30.0)
}
```

El `Sensor` no sabe si al otro lado hay una pantalla, una alarma o un registro en fichero: solo conoce la interfaz. Añadir un receptor nuevo **no obliga a tocar la fuente**.

**Preguntas**

**18.** ¿Cómo permite una interfaz que varias clases actúen como receptoras de la misma fuente?

   a) Obliga a que todas las clases hereden de la fuente
   b) Define un contrato común; la fuente notifica a través de él sin conocer la clase concreta
   c) Hace que la fuente ejecute todas las reacciones dentro de su propio código
   d) Impide que haya más de un receptor registrado a la vez

**19.** Al ejecutar `main()`, ¿cuántas líneas se imprimen?

### Ampliación: de Engelbart a los servidores · Teoría

> [!NOTA] Ampliación (contenido *stretch*)

## Hitos para entender la interacción moderna

<div class="cards">
  <div class="card"><h4>Douglas Engelbart</h4><p>Pionero de la interacción persona-ordenador; asociado al <b>ratón</b> y a la interacción directa.</p></div>
  <div class="card"><h4>Xerox PARC</h4><p>Centro de investigación que impulsó las <b>interfaces gráficas</b> y la interacción moderna.</p></div>
  <div class="card"><h4>WIMP</h4><p><b>W</b>indows · <b>I</b>cons · <b>M</b>enus · <b>P</b>ointer: ventanas, iconos, menús y puntero.</p></div>
</div>

Con una GUI el usuario puede hacer clic en cualquier sitio en cualquier momento: el programa ya no puede seguir un guion fijo. Por eso las interfaces gráficas **popularizaron** la POE… pero **no la definen**.

| Ámbito | Sucesos típicos |
| --- | --- |
| GUI y móviles | Clics, teclas, toques, ciclo de vida, sensores |
| Servidores | Conexiones, peticiones, mensajes, temporizadores |
| IoT y firmware | Sensores, actuadores, GPIO, interrupciones, temporizadores |

> [!IMPORTANTE]
> La POE aparece **donde un sistema reacciona a sucesos que no siguen un orden fijo**, haya o no pantalla.

## Inversión de control en cuatro situaciones

| Situación | Quién decide cuándo se reacciona |
| --- | --- |
| **Botón** | El usuario decide el clic; el framework invoca la reacción registrada. |
| **Temporizador** | La aplicación configura una reacción; el sistema la activa al vencer el tiempo. |
| **Mensaje de red** | La llegada del mensaje determina cuándo se ejecuta su procesamiento. |
| **Sensor** | El cambio medido determina cuándo se comunica un evento a sus consumidores. |

> [!RESUMEN]
> La pregunta siempre es la misma: **¿qué suceso decide cuándo se ejecuta la reacción?**

**Preguntas**

**20.** En el acrónimo WIMP, ¿qué significa la **P**? (en inglés o en español)

**21.** ¿Qué afirmación sobre la GUI y la POE es correcta?

   a) La POE solo existe en programas con interfaz gráfica
   b) La GUI popularizó la POE, pero el paradigma también está en servidores, IoT y firmware
   c) Los servidores no son dirigidos por eventos porque no tienen usuario delante
   d) Xerox PARC inventó la inversión de control para los servidores web

**22.** En un servidor que procesa mensajes de red, ¿qué determina cuándo se ejecuta el procesamiento de un mensaje?

   a) La llegada del mensaje
   b) El orden en que se declararon las funciones
   c) Un bucle que el programador recorre a mano llamando a cada función
   d) El usuario, pulsando un botón

### Repaso: mapa mental y autoevaluación · Repaso

> [!RESUMEN] Chuleta de la Unidad 1
> - **Secuencial**: el programa decide el orden (1 → 2 → 3). **Dirigido por eventos**: el entorno decide cuándo llega cada suceso y la app reacciona.
> - **POO** organiza piezas (objetos, estado, métodos); **POE** organiza reacciones (sucesos, fuentes, receptores, reacciones).
> - **Inversión de control**: el entorno llama a mi código. *«No nos llames; nosotros te llamaremos.»*
> - **Evento = tipo + origen + datos + instante.**
> - **Fuente** produce/detecta · **receptor** procesa · **notificación** los conecta · **reacción** es el trabajo del receptor.
> - **Estímulo → detección → interpretación → evento software → reacción.**
> - **Polling**: preguntar repetidamente (simple, gasta CPU, latencia según frecuencia). **Interrupción**: señal que pide atención (CPU libre mientras tanto).
> - **IRQ** = la señal · **ISR** = el código que la atiende · **driver** = traduce el dispositivo para el sistema.
> - **Latencia**: del estímulo a poder reaccionar; se acumula en **todas** las capas.

| Memoriza | Distingue | Explica |
| --- | --- | --- |
| POO organiza objetos; POE organiza reacciones. Evento = tipo + origen + datos + instante. | Fuente produce/detecta · receptor procesa · notificación conecta. | Polling vs. interrupción · IRQ vs. ISR · función del driver · significado de latencia. |

**Ampliación**: interfaces, polimorfismo y composición son las herramientas de POO para implementar POE. Engelbart, Xerox PARC y WIMP explican el origen de las GUI modernas; la POE se generaliza a servidores, IoT y firmware.

**Preguntas**

**23.** Un compañero dice: «La IRQ es la función que escribimos para atender el botón». ¿Qué le corriges?

   a) Nada, es correcto
   b) Esa función es el driver
   c) Esa función es la ISR; la IRQ es la señal o solicitud de interrupción
   d) Esa función es el polling

**24.** ¿Qué ventaja tiene la interrupción frente al polling?

   a) La CPU puede hacer otro trabajo hasta recibir la señal y la detección no depende de una frecuencia de consulta
   b) No necesita ningún tipo de hardware
   c) Elimina toda la latencia del sistema
   d) Permite que el programa decida el orden de los eventos

**25.** ¿En cuáles de estas situaciones hay inversión de control?

   a) Un botón cuyo listener invoca el framework al pulsarlo
   b) Un temporizador que activa una reacción configurada al vencer
   c) Un sensor cuyo cambio dispara la notificación a sus consumidores
   d) Una función que calcula el máximo de una lista y devuelve el resultado

**26.** ¿Cómo se llama el tiempo que transcurre desde que ocurre el estímulo hasta que la aplicación puede reaccionar?

### Solucionario

1. b) El momento en que llegan los eventos  
   El entorno decide **cuándo** llega cada suceso; la aplicación solo reacciona. Por eso el orden de ejecución depende de la llegada de los eventos, no del código fuente.
2. a) Una app móvil que reacciona a los toques en la pantalla; c) Un servidor web que atiende peticiones según van llegando; d) El firmware de un termostato que reacciona a la interrupción de un sensor  
   La app, el servidor y el firmware esperan sucesos que no controlan. El script del CSV decide él solo el orden de todo: es secuencial.
3. `hollywood`  
   Es el **principio de Hollywood**: tú registras la reacción y el entorno te llama cuando toca.
4. c) El entorno (framework, SO, biblioteca o hardware) invoca la reacción que la aplicación registró  
   En IoC la aplicación no decide cuándo se ejecuta su código: lo registra y es el entorno quien lo invoca. La última opción es justo el flujo convencional.
5. b) B; c) C; e) E  
   B (botón), C (temporizador) y E (interrupción de un pin) registran una reacción que invoca otro. A y D son llamadas normales: tu código llama y recibe la respuesta en ese mismo punto.
6. b) El sistema, al vencer el plazo de 5000 ms  
   La aplicación **configura** la reacción; el sistema la **activa** cuando vence el tiempo. Es uno de los cuatro casos clásicos de IoC: botón, temporizador, mensaje de red y sensor.
7. a) Tipo; c) Origen; d) Datos; f) Instante  
   Tipo (qué), origen (quién lo produjo), datos (con qué información) e instante (cuándo). El receptor no forma parte del evento: es quien lo procesa.
8. `receptor`  
   Es el **receptor**: recibe la notificación y ejecuta la reacción.
9. d) Cerrar el cuadro de búsqueda  
   La reacción es el **trabajo** del receptor: cerrar el cuadro. La pulsación es el evento, el teclado forma parte de la fuente y entregar el evento es la notificación.
10. b) Los datos (el código de la tecla)  
   Ambos eventos tienen el mismo tipo (tecla pulsada) y el mismo origen (teclado). Lo que cambia son los **datos**: el código de la tecla.
11. `instante`  
   El **instante**: cuándo ocurrió o se registró el suceso.
12. 1. Estímulo → 2. Detección → 3. Interpretación (driver y SO) → 4. Evento software → 5. Reacción  
   Estímulo → detección (polling o interrupción) → interpretación por driver y SO → evento software con tipo, origen y datos → notificación y reacción de la aplicación.
13. b) Consume CPU y su tiempo de respuesta depende de la frecuencia de consulta  
   Preguntar sin parar gasta CPU; preguntar poco retrasa la detección. La última opción describe justo lo contrario de una interrupción, que **libera** a la CPU.
14. b) El código que ejecuta la CPU para atender una interrupción  
   ISR = *Interrupt Service Routine*, la rutina (código) que atiende la interrupción. La primera opción es la IRQ, la tercera el driver y la cuarta la latencia.
15. a) En la detección (polling o interrupción); b) En el driver; c) En la entrega del evento a la aplicación; e) En la propia reacción  
   La latencia se acumula en detección, driver, transferencia, generación, entrega y reacción. Por eso la opción «únicamente en el código final» es falsa.
16. `100`  
   Si el suceso ocurre justo después de mirar, no se ve hasta la siguiente consulta: casi **100 ms** de latencia de detección.
17. `IRQ`  
   Es la **IRQ** (*Interrupt Request*). El código que la atiende es la ISR.
18. b) Define un contrato común; la fuente notifica a través de él sin conocer la clase concreta  
   La fuente solo conoce `ReceptorTemperatura`. Cualquier clase que implemente ese contrato puede registrarse, y cada una reacciona a su manera (polimorfismo).
19. `2`  
   Se notifica a los dos receptores: `Pantalla` imprime «Ahora: 30.0 °C» y `Alarma(25.0)` imprime la alerta porque 30.0 > 25.0. Total: **2** líneas.
20. `pointer`  
   WIMP = Windows, Icons, Menus, **Pointer** (puntero).
21. b) La GUI popularizó la POE, pero el paradigma también está en servidores, IoT y firmware  
   La GUI es una aplicación muy importante del paradigma, pero la POE se generaliza a cualquier sistema que reaccione a sucesos sin orden fijo: servidores, IoT y firmware.
22. a) La llegada del mensaje  
   En el caso del mensaje de red, es **su llegada** la que dispara el procesamiento: otro ejemplo de inversión de control.
23. c) Esa función es la ISR; la IRQ es la señal o solicitud de interrupción  
   La **IRQ** es la petición (la señal). La **ISR** es la rutina, el código que se ejecuta para atenderla.
24. a) La CPU puede hacer otro trabajo hasta recibir la señal y la detección no depende de una frecuencia de consulta  
   La interrupción libera a la CPU y avisa cuando ocurre el suceso. Pero no elimina **toda** la latencia: driver, entrega y reacción siguen sumando tiempo.
25. a) Un botón cuyo listener invoca el framework al pulsarlo; b) Un temporizador que activa una reacción configurada al vencer; c) Un sensor cuyo cambio dispara la notificación a sus consumidores  
   Botón, temporizador y sensor (y mensaje de red) son los cuatro casos clásicos de IoC. Calcular un máximo es una llamada convencional.
26. `latencia`  
   Es la **latencia**, y depende de todo el recorrido, no solo del código final.

## De agenda.kt a agendaeventos.kt

_Un menú de consola que lo decide todo, reescrito en cuatro piezas: evento, fuente, receptor y un bucle «tonto» que solo pide y entrega. Y dónde está ese bucle en Android._

### Un vocabulario, tres contextos · Teoría

Esta sesión práctica aplica el mismo vocabulario en tres sitios distintos:

| Bloque | Qué se hace | Tiempo |
| --- | --- | --- |
| **pde-plantilla** | Vistas clásicas de Android: un botón, un interruptor y sus listeners. ¿Quién decide el flujo y dónde están las reacciones? | 45 min |
| **pde-consola · agenda.kt** | Un menú de consola secuencial. El programa lo decide todo: pregunta, espera, elige, repite. | 20 min |
| **agendaeventos.kt** | El mismo programa reescrito en piezas separadas: fuente, evento y receptor. | 35 min |
| **Debate** | Diez situaciones de la vida real (lección siguiente). | opcional |

## El vocabulario de hoy

<div class="cards">
  <div class="card"><h4>Fuente</h4><p>Quién origina el suceso: una persona, un sensor, un temporizador, otro programa. En consola: el teclado y la función que lo lee.</p></div>
  <div class="card"><h4>Evento</h4><p>El suceso concreto, con sus datos: «clic en Enviar», «timeout a los 5 s», «mensaje de Ana». En código: un valor de un tipo que lo representa.</p></div>
  <div class="card"><h4>Receptor</h4><p>El código que escucha y reacciona: un listener, una lambda, una función <code>manejar()</code>. Sabe qué hacer, no cuándo le toca.</p></div>
</div>

> [!IMPORTANTE] La pregunta guía de toda la sesión
> **¿Quién decide qué pasa y cuándo?**

En **pde-plantilla** la respuesta ya se intuye: escribís cosas como `boton.setOnClickListener { ... }` y nunca llamáis vosotros a esa lambda. Alguien la llama. En consola vamos a construir ese "alguien" a mano para ver qué hay dentro.

**Preguntas**

**1.** Según el vocabulario de la sesión, ¿qué sabe un receptor?

   a) Cuándo le toca ejecutarse, pero no qué hacer
   b) Qué hacer, pero no cuándo le toca
   c) Qué hacer y cuándo, porque él controla el bucle
   d) Nada: solo reenvía el evento a la fuente

**2.** En un programa de consola, ¿qué hace de fuente de eventos?

   a) La lista de tareas
   b) El `when` que elige la rama
   c) El teclado y la función que lo lee
   d) La función `println`

### agenda.kt: el programa que lo decide todo · Teoría

Este es el punto de partida, en versión plegada (el contenido de las ramas viene después):

```kotlin
fun main() {
    val tareas = mutableListOf<String>()
    var salir = false

    while (!salir) {                                  // 1. el bucle que espera
        println("\n--- Agenda de tareas ---")
        println("1. Añadir tarea")
        println("2. Listar tareas")
        println("3. Eliminar tarea")
        println("4. Salir")
        print("Elige una opción: ")

        when (readLine()?.trim()) {                   // 2. punto de bloqueo  3. el bucle decide
            "1" -> { /* añadir tarea */ }
            "2" -> { /* listar tareas */ }
            "3" -> { /* eliminar tarea */ }
            "4" -> { salir = true; println("Hasta luego.") }  // 4. la única salida
            else -> println("Opción no reconocida.")
        }
    }
}
```

1. **El bucle que espera.** Mientras nadie elija «Salir», el programa vuelve a preguntar. Nadie le avisa de nada: es **él** quien pregunta, una y otra vez.
2. **El punto de bloqueo.** `readLine()` detiene el programa hasta que el usuario pulsa Intro. Es la única fuente de sucesos, y está **enterrada dentro del bucle**.
3. **El bucle decide.** El `when` compara un texto y elige la rama. La lógica de qué hacer vive dentro del propio bucle.
4. **La única salida.** Poner `salir = true` es lo único que rompe el ciclo: el programa termina cuando él mismo lo decide.

Ahora las ramas desplegadas:

```kotlin
"1" -> {
    print("Descripción de la tarea: ")
    val desc = readLine() ?: ""          // ¡vuelve a leer del teclado dentro de la reacción!
    tareas.add(desc)
    println("Tarea añadida.")
}
"2" -> {
    if (tareas.isEmpty()) println("No hay tareas.")
    else tareas.forEachIndexed { i, t -> println("$i: $t") }
}
"3" -> {
    print("Índice a eliminar: ")
    val idx = readLine()?.toIntOrNull()
    if (idx != null && idx in tareas.indices) {
        tareas.removeAt(idx)
        println("Tarea eliminada.")
    } else {
        println("Índice no válido.")
    }
}
```

> [!CUIDADO] Tres problemas de diseño
> - **Fuente y receptor mezclados**: cada rama es una reacción… pero vuelve a leer del teclado. Quien reacciona también decide cuándo pedir más datos.
> - **Estado, reacción y pantalla, todo junto**: la lista se modifica y se imprime desde el mismo sitio.
> - **Para añadir una opción hay que tocar el bucle**: cualquier cambio pasa por el corazón del programa.

**Preguntas**

**3.** ¿Qué llamada de agenda.kt bloquea el programa hasta que el usuario pulsa Intro?

**4.** ¿Qué problemas de diseño tiene agenda.kt?

   a) Las reacciones vuelven a leer del teclado: fuente y receptor mezclados
   b) No compila porque falta el `else` del `when`
   c) Para añadir una opción hay que modificar el bucle principal
   d) El estado se modifica y se muestra desde el mismo sitio

### Ejercicio: agenda.kt en ejecución · Ejercicio

Esto es lo que se ve en la consola de pde-consola tras dos vueltas del bucle:

```text
--- Agenda de tareas ---
1. Añadir tarea
2. Listar tareas
3. Eliminar tarea
4. Salir
Elige una opción: 1
Descripción de la tarea: Estudiar eventos
Tarea añadida.

--- Agenda de tareas ---
1. Añadir tarea
2. Listar tareas
3. Eliminar tarea
4. Salir
Elige una opción: 2
0: Estudiar eventos

--- Agenda de tareas ---
...
Elige una opción: ▌
```

Tres observaciones:

- **Lo que teclea el usuario** es la única información que entra en el programa, y solo se lee **cuando el programa lo pide**, nunca antes.
- **El menú se repinta en cada vuelta**: es el programa quien lleva la iniciativa.
- **El cursor parpadea: programa parado.** Está bloqueado en `readLine()`. No hace nada más hasta que pulses Intro: ni cuenta el tiempo, ni escucha la red, ni lee un sensor.

> [!EXAMEN] Pregunta para la clase
> Si mientras espera llegara un mensaje de red o venciera un temporizador, ¿se enteraría este programa? ¿Qué tendría que cambiar para que sí?

<details><summary>Ver respuesta</summary>
<p>No. Mientras está bloqueado en <code>readLine()</code> el programa solo puede recibir una línea de teclado: el teclado es su <b>única fuente</b> y está enterrada en el bucle. Para enterarse de otros sucesos haría falta separar las fuentes del bucle, que este no se quede bloqueado esperando solo al teclado y que cada suceso llegue como un <b>evento</b> a un receptor. Es justo el camino que empieza <code>agendaeventos.kt</code> y que Android ya resuelve con su propio bucle.</p>
</details>

**Preguntas**

**5.** Con el programa parado en `readLine()`, llega un mensaje de red. ¿Qué ocurre?

   a) El programa interrumpe la lectura y muestra el mensaje
   b) Nada: el programa no se entera, solo puede recibir lo que se teclea
   c) El `when` añade una rama nueva automáticamente
   d) El sistema operativo llama a la rama `else`

**6.** Partiendo de la agenda vacía, el usuario elige 1, escribe «Comprar pan», elige 1 otra vez, escribe «Llamar a Ana» y elige 2. ¿Qué línea se imprime para la segunda tarea?

### agendaeventos.kt (1/4): el evento y el estado · Teoría

Reescribimos el mismo programa separando **cuatro piezas**. El bucle deja de *decidir* y pasa a *entregar*:

<div class="flow">
  <div><b>Fuente</b><small>leerEvento(): lee el teclado y lo traduce. No ejecuta nada.</small></div>
  <div><b>Evento</b><small>EventoAgenda: qué ha pasado y con qué datos</small></div>
  <div><b>Receptor</b><small>manejar(): cambia el estado e imprime. No sabe cuándo le toca.</small></div>
  <div><b>Estado</b><small>EstadoAgenda: lo único que persiste entre eventos</small></div>
</div>

Y alrededor, el **bucle de eventos** en `main()`, que solo pide un evento y lo entrega.

Antes de leer nada del teclado, definimos **qué puede pasar**:

```kotlin
// --- Evento: qué puede pasar, con los datos que necesite cada caso ---
sealed class EventoAgenda {
    data class AnadirTarea(val descripcion: String) : EventoAgenda()
    object ListarTareas : EventoAgenda()
    data class EliminarTarea(val indice: Int) : EventoAgenda()
    object Salir : EventoAgenda()
    object OpcionInvalida : EventoAgenda()
}

// --- Estado: lo único que persiste entre eventos ---
class EstadoAgenda {
    val tareas = mutableListOf<String>()
    var enMarcha = true
}
```

- **Un tipo cerrado de sucesos**: con `sealed class` solo existen estos cinco eventos. El compilador obliga al receptor a tratarlos todos en el `when`.
- **El evento lleva sus datos**: `AnadirTarea` transporta la descripción; `EliminarTarea`, el índice. Ya no hay que volver a preguntar dentro de la reacción.
- **El estado, fuera del bucle**: lo que cambia entre evento y evento. En Android será `remember { mutableStateOf(...) }` o un campo de la Activity.

> [!NOTA] Compáralo con agenda.kt
> Allí «qué ha pasado» era un `String` (`"1"`, `"2"`…) comparado a mano. Aquí es un **valor con nombre y con datos**.

Fíjate en que `ListarTareas` o `Salir` son `object`: no necesitan datos, basta con una única instancia. `AnadirTarea` y `EliminarTarea` son `data class` porque cada evento lleva información distinta.

**Preguntas**

**7.** ¿Qué ventaja da declarar `EventoAgenda` como `sealed class`?

   a) Que los eventos se ejecutan más rápido
   b) Que solo existen los eventos declarados y el compilador obliga a tratarlos todos en el `when`
   c) Que no hace falta escribir un receptor
   d) Que los eventos se guardan automáticamente en disco

**8.** ¿Qué eventos de `EventoAgenda` transportan datos?

   a) `AnadirTarea`
   b) `ListarTareas`
   c) `EliminarTarea`
   d) `Salir`
   e) `OpcionInvalida`

### agendaeventos.kt (2/4 y 3/4): fuente y receptor · Teoría

## La fuente: `leerEvento()`

```kotlin
// --- Fuente: traduce entrada bruta de teclado en un evento con significado ---
fun leerEvento(): EventoAgenda {
    println("\n--- Agenda de tareas ---")
    println("1. Añadir tarea")
    println("2. Listar tareas")
    println("3. Eliminar tarea")
    println("4. Salir")
    print("Elige una opción: ")
    return when (readLine()?.trim()) {
        "1" -> {
            print("Descripción de la tarea: ")
            EventoAgenda.AnadirTarea(readLine() ?: "")
        }
        "2" -> EventoAgenda.ListarTareas
        "3" -> {
            print("Índice a eliminar: ")
            EventoAgenda.EliminarTarea(readLine()?.toIntOrNull() ?: -1)
        }
        "4" -> EventoAgenda.Salir
        else -> EventoAgenda.OpcionInvalida
    }
}
```

- **Sigue leyendo del teclado**, pero es el **único sitio** del programa donde ocurre: todo `readLine()` vive aquí.
- **Traduce, no ejecuta**: pide los datos que el evento necesita y lo construye. No añade, no borra, no lista.
- **Hasta el error es un evento**: una opción desconocida se devuelve como `OpcionInvalida` y ya decidirá el receptor.

## El receptor: `manejar()`

```kotlin
// --- Receptor: un manejador por tipo de evento ---
fun manejar(evento: EventoAgenda, estado: EstadoAgenda) {
    when (evento) {
        is EventoAgenda.AnadirTarea -> {
            estado.tareas.add(evento.descripcion)
            println("Tarea añadida.")
        }
        is EventoAgenda.ListarTareas -> {
            if (estado.tareas.isEmpty()) println("No hay tareas.")
            else estado.tareas.forEachIndexed { i, t -> println("$i: $t") }
        }
        is EventoAgenda.EliminarTarea -> {
            if (evento.indice in estado.tareas.indices) {
                estado.tareas.removeAt(evento.indice)
                println("Tarea eliminada.")
            } else {
                println("Índice no válido.")
            }
        }
        is EventoAgenda.Salir -> {
            estado.enMarcha = false
            println("Hasta luego.")
        }
        is EventoAgenda.OpcionInvalida -> println("Opción no reconocida.")
    }
}
```

- Un `when` sobre el **tipo** de evento: cinco eventos, cinco reacciones. Si olvidas uno, no compila.
- **Solo toca el estado** e imprime. **No lee del teclado**: los datos ya vienen dentro del evento (`evento.indice` ya está listo).
- **Salir también es una reacción**: no hay `break` ni `return` escondido; cambia `enMarcha` y el bucle terminará solo.

**Preguntas**

**9.** En agendaeventos.kt, ¿en qué función(es) aparece `readLine()`?

   a) En `leerEvento()` y en `manejar()`
   b) Solo en `main()`
   c) Solo en `leerEvento()`
   d) En las tres funciones

**10.** ¿Cómo consigue el evento `Salir` que termine el programa?

   a) Llamando a `exitProcess(0)` dentro de `manejar()`
   b) Con un `break` escondido en la fuente
   c) Cambiando `estado.enMarcha` a `false`, de modo que el bucle termina solo
   d) Lanzando una excepción que captura `main()`

### agendaeventos.kt (4/4): el bucle «tonto» · Teoría

```kotlin
// --- Bucle de eventos: ya no decide QUÉ hacer,
//     solo obtiene un evento y lo entrega ---
fun main() {
    val estado = EstadoAgenda()
    while (estado.enMarcha) {
        val evento = leerEvento()   // pide un evento
        manejar(evento, estado)     // lo entrega
    }
}
```

Cuatro líneas que **no saben nada de tareas**:

1. **Sigue habiendo un `while`.** Estamos en consola: nadie nos avisa, así que alguien tiene que preguntar. Pero el bucle ya no contiene ni un solo `"1"`, `"2"` o `"3"`.
2. **Pide un evento.** La fuente se encarga de esperar y traducir.
3. **Lo entrega.** El receptor se encarga de reaccionar. `main()` no sabe qué significa ninguno de los eventos.

Hemos pasado **de un bucle que decide a un bucle que entrega**. Añadir una opción nueva ya no toca esta parte.

![Arriba, agenda.kt: dentro de un único while (!salir), readLine() bloquea el programa, el when elige la rama entre «1» y «4» y las reacciones tocan estado y pantalla y vuelven a llamar a readLine(); después el bucle vuelve a preguntar. Abajo, agendaeventos.kt: el bucle while (estado.enMarcha) manejar(leerEvento(), estado) primero pide un evento a la fuente leerEvento(), que contiene el único readLine() y produce un EventoAgenda, y después lo entrega al receptor manejar(), que modifica EstadoAgenda (tareas, enMarcha). En naranja, los sitios donde se lee el teclado.](/img/pde/tema-1/agenda-vs-agendaeventos.svg "De un bucle que lo decide todo a un bucle que solo pide y entrega (en naranja, dónde se lee el teclado)")

## ¿Y en pde-plantilla, dónde está este `while`?

**No está en vuestro código.** Vive dentro de Android: es el **bucle de mensajes del hilo principal**. Vosotros solo escribís los receptores.

| Consola (agendaeventos.kt) | Android (pde-plantilla) |
| --- | --- |
| `while (estado.enMarcha) { ... }` escrito a mano | Bucle de mensajes del hilo principal, dentro de Android |
| `leerEvento()` | El sistema detecta toques y teclas y genera los eventos |
| `manejar(evento, estado)` | `setOnClickListener { ... }`: **vuestro manejar()** |
| `EstadoAgenda` | Campos de la Activity o `remember { mutableStateOf(...) }` |

> [!ANALOGIA]
> Lo que en consola habéis construido a mano, en Android **ya viene hecho**, y por eso parece que «nadie» decide el flujo. Sí hay alguien: el bucle del framework. Es la inversión de control de la lección anterior hecha código.

**Preguntas**

**11.** En una app Android, ¿dónde está el equivalente al `while` de `main()`?

   a) En el método `onCreate` que escribe el alumno
   b) Dentro de Android: el bucle de mensajes del hilo principal
   c) No existe: en Android no hay ningún bucle
   d) En cada `setOnClickListener`, que lleva su propio `while`

**12.** En Android, ¿qué llamada que usáis en pde-plantilla cumple el papel de vuestro `manejar()` para un botón?

### Ejercicio: ¿a qué pieza pertenece? · Ejercicio

Para cada trozo de código, decide a qué pieza de agendaeventos.kt pertenece: **fuente**, **evento**, **receptor**, **estado** o **bucle**.

| # | Fragmento |
| --- | --- |
| 1 | `EventoAgenda.EliminarTarea(readLine()?.toIntOrNull() ?: -1)` |
| 2 | `data class AnadirTarea(val descripcion: String) : EventoAgenda()` |
| 3 | `estado.tareas.removeAt(evento.indice)` |
| 4 | `var enMarcha = true` |
| 5 | `manejar(leerEvento(), estado)` dentro de `while (estado.enMarcha)` |
| 6 | `else -> EventoAgenda.OpcionInvalida` |

<details><summary>Ver solución</summary>
<p>1 → <b>fuente</b> (lee el teclado y construye el evento). 2 → <b>evento</b> (definición del tipo con sus datos). 3 → <b>receptor</b> (reacción que cambia el estado). 4 → <b>estado</b> (persiste entre eventos). 5 → <b>bucle</b> (pide y entrega). 6 → <b>fuente</b> (traduce una entrada desconocida en un evento; no la trata).</p>
</details>

> [!TRUCO] Cómo decidirlo rápido
> ¿Toca el teclado? Fuente. ¿Define *qué puede pasar*? Evento. ¿Cambia la lista o imprime el resultado? Receptor. ¿Sobrevive entre vuelta y vuelta? Estado. ¿Solo pide y entrega? Bucle.

**Preguntas**

**13.** ¿A qué pieza pertenece el fragmento 1, `EventoAgenda.EliminarTarea(readLine()?.toIntOrNull() ?: -1)`?

**14.** ¿Y el fragmento 3, `estado.tareas.removeAt(evento.indice)`?

   a) Fuente
   b) Evento
   c) Bucle
   d) Receptor

**15.** El fragmento 6, `else -> EventoAgenda.OpcionInvalida`, ¿en qué pieza está y por qué?

   a) En la fuente: traduce una entrada desconocida en un evento y deja que el receptor decida
   b) En el receptor: imprime «Opción no reconocida.»
   c) En el bucle: es la condición de salida
   d) En el estado: guarda el último error

### Ejercicio: añadir «Vaciar agenda» y predecir · Ejercicio

Queremos una opción nueva, **«5. Vaciar agenda»**, que borre todas las tareas.

**En agenda.kt** habría que meter una rama `"5" -> { ... }` en el `when` del bucle principal: el corazón del programa.

**En agendaeventos.kt** los cambios quedan repartidos así:

```kotlin
// Evento: un caso más
object VaciarAgenda : EventoAgenda()

// Fuente: una opción más en el menú y en la traducción
println("5. Vaciar agenda")
"5" -> EventoAgenda.VaciarAgenda

// Receptor: una reacción más
is EventoAgenda.VaciarAgenda -> {
    estado.tareas.clear()
    println("Agenda vaciada.")
}

// Bucle main(): ... ¡nada!
```

En un programa dirigido por eventos **basta con añadir un evento y su receptor** (y, en consola, enseñar a la fuente a producirlo). El bucle no se entera.

## Predice la salida

Con la agenda **vacía**, el usuario teclea `3` y, cuando le piden el índice, escribe `abc`.

Sigue el evento: `readLine()?.toIntOrNull()` devuelve `null` → el operador `?:` lo cambia por `-1` → la fuente devuelve `EliminarTarea(-1)` → el receptor comprueba si `-1` está en `estado.tareas.indices`…

**Preguntas**

**16.** Para añadir «Vaciar agenda» a agendaeventos.kt, ¿qué partes hay que modificar?

   a) La `sealed class EventoAgenda`
   b) La fuente `leerEvento()`
   c) El receptor `manejar()`
   d) El bucle de `main()`

**17.** ¿Qué imprime el receptor en el caso de `3` seguido de `abc`?

**18.** Ordena lo que ocurre en una vuelta del bucle cuando el usuario elige «2».

   Ordena: El receptor imprime la lista (o «No hay tareas.») · La fuente imprime el menú y se bloquea en `readLine()` · La fuente traduce «2» en `EventoAgenda.ListarTareas` · `main()` entrega el evento a `manejar()` · `main()` llama a `leerEvento()`

### Repaso: de un bucle que decide a un bucle que entrega · Repaso

> [!RESUMEN] Chuleta
> - **agenda.kt**: un `while` que lo decide todo. `readLine()` bloquea; es la única fuente y está dentro del bucle. Las reacciones vuelven a leer del teclado y mezclan estado y pantalla. Añadir una opción obliga a tocar el bucle.
> - **agendaeventos.kt** en cuatro piezas:
>   - **Evento**: `sealed class EventoAgenda` (tipo cerrado, cada caso con sus datos).
>   - **Fuente**: `leerEvento()`, único sitio con `readLine()`; traduce, no ejecuta; hasta el error es un evento.
>   - **Receptor**: `manejar()`, un `when` por tipo; solo toca el estado; sabe qué hacer, no cuándo.
>   - **Estado**: `EstadoAgenda` (`tareas`, `enMarcha`).
>   - **Bucle**: `while (estado.enMarcha) manejar(leerEvento(), estado)`.
> - **Android**: el bucle es el de mensajes del hilo principal; `setOnClickListener { … }` es vuestro `manejar()`.

| | agenda.kt | agendaeventos.kt |
| --- | --- | --- |
| «Qué ha pasado» | Un `String` comparado a mano | Un valor con nombre y datos |
| Dónde se lee el teclado | En el bucle y en las reacciones | Solo en la fuente |
| Añadir una opción | Tocar el bucle | Evento + traducción + reacción |
| Quién decide qué hacer | El bucle | El receptor |

**Preguntas**

**19.** En agendaeventos.kt, ¿quién decide **qué** hacer con cada suceso?

   a) El bucle de `main()`
   b) La fuente `leerEvento()`
   c) El receptor `manejar()`
   d) La clase `EstadoAgenda`

**20.** ¿Qué equivale en Android a `EstadoAgenda`?

   a) El bucle de mensajes del hilo principal
   b) Un campo de la Activity o `remember { mutableStateOf(...) }`
   c) El método `setOnClickListener`
   d) El fichero `AndroidManifest.xml`

**21.** ¿Por qué agendaeventos.kt sigue necesitando un `while`?

   a) Porque Kotlin no permite programas sin bucles
   b) Porque en consola nadie nos avisa: alguien tiene que preguntar por el siguiente suceso
   c) Porque la `sealed class` lo exige
   d) Por error: debería eliminarse

### Solucionario

1. b) Qué hacer, pero no cuándo le toca  
   El receptor contiene la reacción (**qué** hacer); **cuándo** se ejecuta lo decide quien le entrega el evento.
2. c) El teclado y la función que lo lee  
   En consola, los sucesos llegan del **teclado**, y la función que lo lee (con `readLine()`) es la que los origina en el programa.
3. `readLine()`  
   `readLine()` detiene el programa hasta recibir una línea. Es la única fuente de sucesos y está metida dentro del bucle.
4. a) Las reacciones vuelven a leer del teclado: fuente y receptor mezclados; c) Para añadir una opción hay que modificar el bucle principal; d) El estado se modifica y se muestra desde el mismo sitio  
   El `when` sí tiene `else`. Los problemas son de diseño: fuente y receptor mezclados, estado/reacción/pantalla juntos y un bucle que hay que tocar ante cualquier cambio.
5. b) Nada: el programa no se entera, solo puede recibir lo que se teclea  
   El teclado es la única fuente y el programa está bloqueado esperándola. Ni temporizadores ni red ni sensores pueden llegar a él.
6. `1: Llamar a Ana`  
   `forEachIndexed` numera desde 0: se imprime «0: Comprar pan» y **«1: Llamar a Ana»**.
7. b) Que solo existen los eventos declarados y el compilador obliga a tratarlos todos en el `when`  
   Un tipo cerrado: no puede aparecer un sexto evento por sorpresa y, si el receptor olvida uno, no compila.
8. a) `AnadirTarea`; c) `EliminarTarea`  
   `AnadirTarea` lleva la descripción y `EliminarTarea` el índice. Los demás son `object` sin datos.
9. c) Solo en `leerEvento()`  
   La fuente es el único sitio que toca el teclado. El receptor recibe los datos ya empaquetados dentro del evento.
10. c) Cambiando `estado.enMarcha` a `false`, de modo que el bucle termina solo  
   Salir es una reacción más: modifica el estado (`enMarcha = false`) y la condición del `while` hace el resto.
11. b) Dentro de Android: el bucle de mensajes del hilo principal  
   El bucle sigue existiendo, pero lo escribe el framework: es el bucle de mensajes del hilo principal. Vosotros solo aportáis los receptores.
12. `setOnClickListener`  
   `setOnClickListener { … }` registra el receptor del clic: es vuestro `manejar()`.
13. `fuente`  
   Lee del teclado y **construye** el evento con su dato: es trabajo de la **fuente**.
14. d) Receptor  
   Usa el dato que trae el evento para cambiar el estado: es una **reacción**, parte del receptor `manejar()`.
15. a) En la fuente: traduce una entrada desconocida en un evento y deja que el receptor decida  
   Hasta el error es un evento. La fuente no lo trata: lo devuelve como `OpcionInvalida` y el receptor imprime el mensaje.
16. a) La `sealed class EventoAgenda`; b) La fuente `leerEvento()`; c) El receptor `manejar()`  
   Evento nuevo, traducción nueva en la fuente y reacción nueva en el receptor. `main()` no cambia: solo pide y entrega.
17. `Índice no válido.`  
   `-1` no está en los índices de la lista, así que el receptor imprime **«Índice no válido.»**
18. 1. `main()` llama a `leerEvento()` → 2. La fuente imprime el menú y se bloquea en `readLine()` → 3. La fuente traduce «2» en `EventoAgenda.ListarTareas` → 4. `main()` entrega el evento a `manejar()` → 5. El receptor imprime la lista (o «No hay tareas.»)  
   El bucle pide → la fuente espera y traduce → el bucle entrega → el receptor reacciona.
19. c) El receptor `manejar()`  
   El bucle solo pide y entrega; la fuente solo traduce. La decisión de qué hacer vive en el receptor.
20. b) Un campo de la Activity o `remember { mutableStateOf(...) }`  
   El estado es lo que persiste entre eventos: en Android, un campo de la Activity o un estado de Compose con `remember { mutableStateOf(...) }`.
21. b) Porque en consola nadie nos avisa: alguien tiene que preguntar por el siguiente suceso  
   En consola no hay framework que entregue eventos, así que el bucle lo escribimos nosotros. En Android lo escribe el sistema.

## Debate: ¿fuente, evento o receptor?

_Diez situaciones de la vida real (clics, timeouts, mensajes, sensores y cambios de datos) para practicar la clasificación fuente / evento / receptor y descubrir quién escribe el bucle._

### Las reglas del debate · Ejercicio

Diez situaciones cotidianas, agrupadas en cinco familias: **clic**, **timeout**, **mensaje recibido**, **sensor** y **cambio de datos**. Para cada una hay que rellenar tres columnas:

| Fuente | Evento | Receptor |
| --- | --- | --- |
| ¿Quién origina el suceso? | ¿Qué ocurre exactamente, y con qué datos? | ¿Qué código está escuchando y reacciona? |

> [!IMPORTANTE] Regla del debate
> **Las tres columnas tienen que ser cosas distintas.** Si fuente y evento os salen iguales, revisadlo. Y una pregunta extra: **¿quién escribió el receptor: vosotros, el framework o el sistema operativo?**

Intenta resolver cada tarjeta **antes** de girarla:

> _Aquí hay un simulador interactivo (`fer-cards`): ábrelo en la web._
En los pasos siguientes repasaremos las diez situaciones con las respuestas del profesor, de dos en dos o de cuatro en cuatro, y nos fijaremos en las trampas típicas: confundir la fuente con el canal, o el evento con el dispositivo que lo produce.

**Preguntas**

**1.** Un grupo propone para «el usuario pulsa Enviar»: fuente = «el botón Enviar», evento = «el botón Enviar», receptor = «el formulario». ¿Qué falla?

   a) Nada, es correcto
   b) Fuente y evento son iguales: el evento debe ser lo que ocurre (el clic) con sus datos
   c) El receptor debería ser el usuario
   d) Falta indicar el sistema operativo como evento

**2.** ¿Cuáles de estas son familias de situaciones del debate?

   a) Clic
   b) Compilación
   c) Timeout
   d) Sensor
   e) Cambio de datos
   f) Herencia

### Situaciones 1-4: clics y timeouts · Ejercicio

| # | Situación | Fuente | Evento | Receptor |
| --- | --- | --- | --- | --- |
| 01 | El usuario pulsa «Enviar» en un formulario web. | El usuario, a través del ratón o la pantalla táctil, y el navegador que capta el clic. | Clic en el botón «Enviar», con el elemento pulsado y los datos del formulario. | El manejador `onSubmit` / `addEventListener('click', …)` que valida y envía la petición. |
| 02 | Doble clic sobre un icono del escritorio. | El usuario + el controlador del ratón / sistema operativo, que detecta el patrón de doble clic. | «Doble clic sobre el icono X». | El manejador del escritorio que abre la aplicación asociada. |
| 03 | Una petición HTTP no recibe respuesta en 5 segundos. | El temporizador (timer) del cliente HTTP. | Vencimiento del plazo de espera a los 5000 ms. | El callback `onTimeout` / bloque `catch` que cancela la petición y muestra un error. |
| 04 | Una sesión se cierra sola tras 15 minutos de inactividad. | El temporizador de inactividad del servidor o de la aplicación. | Expiración de la sesión. | El manejador que invalida la sesión y redirige al login. |

**Qué aprender de estas cuatro:**

- En los **clics** la fuente no es solo "el usuario": hay una pieza que **capta** el suceso (navegador, controlador del ratón, SO). En el doble clic, además, alguien tiene que **reconocer el patrón** de dos clics seguidos.
- En los **timeouts** no hay ninguna persona: la fuente es un **temporizador**. El evento es justamente que *no* ha pasado nada durante un plazo.

> [!NOTA] ¿Quién escribió el receptor?
> En la 01, el manejador del formulario lo escribe quien desarrolla la web. En la 02, el manejador que abre la aplicación forma parte del **escritorio del sistema operativo**: nadie de vosotros lo ha escrito.

**Preguntas**

**3.** En la situación 3 (petición HTTP sin respuesta en 5 s), ¿quién es la fuente?

   a) El servidor que no responde
   b) El usuario que hizo la petición
   c) El temporizador del cliente HTTP
   d) El bloque `catch`

**4.** En la situación 1, ¿cuál es el receptor?

   a) El navegador que capta el clic
   b) El manejador `onSubmit` / `addEventListener('click', …)` que valida y envía
   c) Los datos del formulario
   d) El ratón

**5.** En la situación 4, completa el evento: «______ de la sesión».

### Situaciones 5-6: mensajes recibidos · Ejercicio

| # | Situación | Fuente | Evento | Receptor |
| --- | --- | --- | --- | --- |
| 05 | Llega un mensaje nuevo de WhatsApp al móvil. | El servidor de mensajería, que empuja la notificación al dispositivo. | «Mensaje entrante», con remitente y contenido. | El servicio de notificaciones de la app, que muestra el aviso. |
| 06 | Un microservicio recibe en una cola (Kafka / RabbitMQ) el aviso de que un pedido se ha pagado. | El servicio de pagos, que publica el mensaje en la cola. | Mensaje «PedidoPagado» con el identificador del pedido. | El consumidor (listener) suscrito a la cola, que actualiza el estado del pedido. |

**Qué aprender de estas dos:**

- En la **05**, desde el punto de vista del móvil, la fuente inmediata es el **servidor de mensajería** que empuja la notificación. Tu amigo escribió el mensaje, pero el suceso que llega al dispositivo lo origina el servidor.
- En la **06** aparece una trampa clásica: **la cola no es la fuente**. Kafka o RabbitMQ son el **canal** por el que viaja la notificación. Quien origina el suceso es el **servicio de pagos**, que *publica*; quien reacciona es el **consumidor suscrito**.

<div class="flow">
  <div><b>Servicio de pagos</b><small>fuente: publica</small></div>
  <div><b>Cola Kafka / RabbitMQ</b><small>canal: transporta</small></div>
  <div><b>Consumidor</b><small>receptor: actualiza el pedido</small></div>
</div>

> [!EXAMEN]
> Distingue siempre **quién origina** el suceso de **por dónde viaja**. Este patrón de publicar / suscribirse lo veréis con detalle en la Unidad 2 (pub/sub).

**Preguntas**

**6.** En la situación 6 (Kafka / RabbitMQ), ¿quién es la fuente?

   a) La cola de Kafka / RabbitMQ
   b) El consumidor suscrito a la cola
   c) El servicio de pagos, que publica el mensaje
   d) El mensaje «PedidoPagado»

**7.** ¿Qué dato lleva el evento «PedidoPagado», según la respuesta del profesor?

**8.** En la situación 5 (WhatsApp), ¿qué datos acompañan al evento «Mensaje entrante»?

   a) Remitente
   b) Contenido
   c) Nivel de batería del móvil
   d) Orientación de la pantalla

### Situaciones 7-8: sensores · Ejercicio

| # | Situación | Fuente | Evento | Receptor |
| --- | --- | --- | --- | --- |
| 07 | El acelerómetro del móvil detecta que has girado la pantalla. | El sensor de orientación (hardware) y su controlador. | Cambio de orientación: de vertical a horizontal. | El `OrientationEventListener` / el sistema de diseño que recoloca la interfaz. |
| 08 | El sensor de temperatura de una nevera inteligente detecta que sube por encima del umbral. | El sensor de temperatura del electrodoméstico. | «Temperatura > umbral», con el valor leído. | El manejador que dispara una alerta al móvil del usuario. |

**Qué aprender de estas dos:**

- Aquí la fuente es **hardware**. Conecta con la lección 1: estímulo físico → detección (polling o interrupción) → driver/controlador → evento software → reacción. En la 07 la respuesta menciona explícitamente "el sensor **y su controlador**".
- El evento no es "el sensor", sino **lo que ha cambiado** y con qué datos: la nueva orientación, o el valor de temperatura que ha superado el umbral.
- En la 08 el evento no es cada lectura, sino un suceso **con significado**: cruzar el umbral. Alguien ha convertido muchas medidas en un único evento relevante.

> [!NOTA] ¿Quién escribió el receptor?
> En la 07 hay dos posibilidades, y la respuesta del profesor recoge ambas: si registras un `OrientationEventListener`, el receptor lo escribes **tú**; si dejas que el sistema recoloque la interfaz, el receptor es del **framework**.

**Preguntas**

**9.** En la situación 7, ¿cuál es el **evento**?

   a) El acelerómetro
   b) El cambio de orientación de vertical a horizontal
   c) El `OrientationEventListener`
   d) La interfaz recolocada

**10.** Ordena la cadena de la situación 8, de la fuente a la reacción.

   Ordena: El manejador recibe el evento · El sensor de temperatura de la nevera mide · Se dispara una alerta al móvil del usuario · Se produce el evento «Temperatura > umbral» con el valor leído

### Situaciones 9-10: cambios de datos · Ejercicio

| # | Situación | Fuente | Evento | Receptor |
| --- | --- | --- | --- | --- |
| 09 | Alguien edita una celda de una hoja de cálculo colaborativa y los demás lo ven al momento. | El usuario que edita + el servidor de sincronización que propaga el cambio. | «Celda A1 modificada», con el nuevo valor. | El listener de cada cliente conectado, que repinta esa celda. |
| 10 | Añades un producto al carrito y el icono del carrito cambia de número. | El propio código de la app: el clic en «Añadir al carrito» modificó una variable de estado. | Cambio del estado *carrito* (nueva lista / tamaño). | La parte de la interfaz que observa ese estado (icono con contador) y se recompone o se repinta. |

**Qué aprender de estas dos:**

- En la **09** un único evento tiene **muchos receptores**: el listener de *cada* cliente conectado. La fuente es doble: quien edita y el servidor que propaga.
- La **10** es la más sutil: la fuente es **tu propio código**. El clic provocó un cambio de estado, y ese **cambio de estado es a su vez un evento** que escucha otra parte de la interfaz. Un evento produce otro.

> [!IMPORTANTE] El estado como fuente de eventos
> Esta idea (una parte de la interfaz que **observa** un estado y se repinta sola cuando cambia) es la base del patrón Observer y de MVVM, que llegarán en la Unidad 2. En pde-plantilla, de momento, la llamáis a mano con `actualizarInterfaz()`.

**Preguntas**

**11.** En la situación 10 (carrito), ¿cuál es la fuente según la respuesta del profesor?

   a) El servidor de la tienda
   b) El propio código de la app, que modificó una variable de estado tras el clic
   c) El icono del carrito
   d) El sistema operativo del móvil

**12.** En la situación 9, ¿cuántos receptores tiene el evento «Celda A1 modificada»?

   a) Ninguno: el servidor repinta todas las pantallas
   b) Solo uno: el usuario que editó
   c) Uno por cada cliente conectado: el listener de cada uno repinta la celda
   d) Exactamente dos: el servidor y el editor

### ¿Quién escribe el bucle? · Teoría

Tras pasar por pde-plantilla, pde-consola y la vida real, aparece siempre lo mismo:

<div class="cards">
  <div class="card"><h4>pde-plantilla</h4><p>El bucle existe, pero <b>vive dentro de Android</b>. Vosotros escribís los receptores (los listeners)… y <code>actualizarInterfaz()</code> a mano.</p></div>
  <div class="card"><h4>pde-consola</h4><p>El bucle lo habéis <b>escrito vosotros</b> con un <code>while</code>. Primero decidía todo; después solo pide un evento y lo entrega.</p></div>
  <div class="card"><h4>Vida real</h4><p><b>No hay bucle de código</b>: el mundo (personas, sensores, redes, temporizadores) genera eventos sin parar. Alguien tiene que escuchar.</p></div>
</div>

<div class="flow">
  <div><b>Fuente</b><small>quién origina</small></div>
  <div><b>Evento</b><small>qué pasa + datos</small></div>
  <div><b>Receptor</b><small>quién reacciona</small></div>
</div>

**Misma estructura en los tres: fuente → evento → receptor.** Solo cambia **dónde vive el bucle** que busca el siguiente suceso y **quién repinta la pantalla**.

![Tres columnas con la misma cadena fuente, evento y receptor. pde-plantilla (Android): usuario y pantalla, clic en el botón, setOnClickListener; el bucle está dentro de Android, es el bucle de mensajes. pde-consola (agendaeventos.kt): leerEvento(), EventoAgenda, manejar(); el bucle lo escribís vosotros con un while que pide y entrega. Vida real (nevera inteligente): sensor de temperatura, temperatura por encima del umbral, manejador de alerta; no hay bucle de código, el mundo genera los eventos. Debajo: el bucle siempre existe; la pregunta es quién lo escribe.](/img/pde/tema-1/quien-escribe-el-bucle.svg "Misma cadena en los tres contextos; solo cambia quién escribe el bucle")

## ¿Quién escribió el receptor?

Repasa las situaciones con esta pregunta:

| Quién lo escribe | Ejemplos del debate |
| --- | --- |
| **Vosotros** (la aplicación) | El `onSubmit` del formulario (01), el consumidor de «PedidoPagado» (06), el manejador que alerta de la nevera (08) |
| **El framework** | El sistema de diseño que recoloca la interfaz al girar (07, si no registras listener propio) |
| **El sistema operativo** | El manejador del escritorio que abre la aplicación (02) |

> [!RESUMEN] Para cerrar
> **El bucle siempre existe. La pregunta es quién lo escribe.**

**Preguntas**

**13.** ¿En qué contexto **habéis escrito vosotros** el bucle de eventos?

   a) pde-plantilla (Android)
   b) pde-consola (agendaeventos.kt)
   c) La vida real
   d) En ninguno

**14.** Completa la frase de cierre: «El bucle siempre existe. La pregunta es quién lo ______.»

### Repaso del debate · Repaso

> [!RESUMEN] Chuleta del debate
> - **Fuente**: quién origina (persona + dispositivo que capta, temporizador, servidor que publica, sensor, o el propio código al cambiar estado).
> - **Evento**: qué ocurre exactamente **y con qué datos** (elemento pulsado, 5000 ms, remitente y contenido, id del pedido, valor leído, nuevo valor de la celda).
> - **Receptor**: el código que escucha y reacciona (manejador, callback, listener, consumidor, interfaz que observa).
> - Las tres columnas **deben ser distintas**.
> - Un **canal** (cola, red) no es la fuente: solo transporta.
> - Un evento puede tener **muchos receptores** (hoja colaborativa) y un evento puede **provocar otro** (carrito).
> - Misma estructura siempre; cambia **dónde vive el bucle** y **quién escribe el receptor**.

| Familia | Fuente típica |
| --- | --- |
| Clic | Usuario + dispositivo / navegador / SO |
| Timeout | Temporizador |
| Mensaje recibido | Servidor o servicio que publica |
| Sensor | Hardware + controlador |
| Cambio de datos | Quien edita / el propio estado de la app |

**Preguntas**

**15.** En una situación de tipo **timeout**, ¿qué suele ser la fuente?

   a) El usuario
   b) Un temporizador
   c) El receptor
   d) La cola de mensajes

**16.** Nueva situación: «La alarma del móvil suena a las 7:00 y la app muestra el botón de posponer». ¿Qué clasificación respeta la regla del debate?

   a) Fuente: la alarma · Evento: la alarma · Receptor: el móvil
   b) Fuente: el temporizador del sistema · Evento: «hora programada alcanzada (7:00)» · Receptor: el manejador de la app que muestra la pantalla de alarma
   c) Fuente: el usuario · Evento: el botón posponer · Receptor: el temporizador
   d) Fuente: la pantalla · Evento: las 7:00 · Receptor: el usuario

**17.** ¿Qué papel juega la cola Kafka en la situación 6?

   a) Es la fuente del evento
   b) Es el receptor que actualiza el pedido
   c) Es el canal por el que viaja la notificación entre fuente y receptor
   d) Es el propio evento

### Solucionario

1. b) Fuente y evento son iguales: el evento debe ser lo que ocurre (el clic) con sus datos  
   Si dos columnas coinciden, algo falla. El evento es **el clic en Enviar**, con el elemento pulsado y los datos del formulario.
2. a) Clic; c) Timeout; d) Sensor; e) Cambio de datos  
   Las cinco familias son clic, timeout, mensaje recibido, sensor y cambio de datos.
3. c) El temporizador del cliente HTTP  
   La fuente es el **timer del cliente HTTP**: es quien origina el suceso «ha vencido el plazo». El `catch` / `onTimeout` es el receptor.
4. b) El manejador `onSubmit` / `addEventListener('click', …)` que valida y envía  
   El receptor es el código que escucha y reacciona: el manejador que valida y envía la petición.
5. `expiración`  
   El evento es la **expiración de la sesión**; la fuente, el temporizador de inactividad; el receptor, el manejador que la invalida y redirige al login.
6. c) El servicio de pagos, que publica el mensaje  
   La fuente es el **servicio de pagos**. La cola es el canal, «PedidoPagado» es el evento y el consumidor es el receptor.
7. `el identificador del pedido`  
   El mensaje «PedidoPagado» lleva el **identificador del pedido**, para que el consumidor sepa qué pedido actualizar.
8. a) Remitente; b) Contenido  
   El evento «mensaje entrante» lleva **remitente y contenido**.
9. b) El cambio de orientación de vertical a horizontal  
   El acelerómetro (y su controlador) es la fuente; el `OrientationEventListener` o el sistema de diseño, el receptor; la interfaz recolocada, la reacción. El evento es el **cambio de orientación**.
10. 1. El sensor de temperatura de la nevera mide → 2. Se produce el evento «Temperatura > umbral» con el valor leído → 3. El manejador recibe el evento → 4. Se dispara una alerta al móvil del usuario  
   Fuente (sensor) → evento con su dato → receptor (manejador) → reacción (alerta al móvil).
11. b) El propio código de la app, que modificó una variable de estado tras el clic  
   La fuente es el propio código: el clic modificó el estado *carrito* y ese cambio es el evento que observa el icono con el contador.
12. c) Uno por cada cliente conectado: el listener de cada uno repinta la celda  
   Cada cliente conectado tiene su listener, que repinta la celda. Un evento, muchos receptores.
13. b) pde-consola (agendaeventos.kt)  
   En consola el `while` es vuestro. En Android vive dentro del framework y en la vida real no hay bucle de código: el mundo genera eventos.
14. `escribe`  
   **El bucle siempre existe. La pregunta es quién lo escribe**: vosotros (consola), el framework (Android) o nadie en código (el mundo real, donde alguien tiene que escuchar).
15. b) Un temporizador  
   En las situaciones 3 y 4 la fuente es un temporizador (del cliente HTTP o de inactividad).
16. b) Fuente: el temporizador del sistema · Evento: «hora programada alcanzada (7:00)» · Receptor: el manejador de la app que muestra la pantalla de alarma  
   Es un caso de timeout: la fuente es un temporizador, el evento es el vencimiento con su dato (la hora) y el receptor es el código que reacciona.
17. c) Es el canal por el que viaja la notificación entre fuente y receptor  
   La cola transporta el mensaje. Fuente: servicio de pagos; evento: «PedidoPagado»; receptor: el consumidor suscrito.

---

# Tema 2 · Callbacks, listeners, handlers y despacho

Entregar código para que otro lo invoque: callbacks síncronos y diferidos, listeners, ciclo de vida, tablas de despacho, Observer, pub/sub, MVVM y todo lo que puede salir mal al notificar.

## Callbacks y listeners

_Qué es entregar una función para que otro la llame, las cuatro formas de escribirla, clausuras, callbacks síncronos frente a diferidos y cómo una fuente gestiona su lista de listeners._

### Entregar, no ejecutar · Teoría

En la Unidad 1 decíamos alegremente «el framework me llama». Hoy toca saber **exactamente qué le entregas y cuándo lo ejecuta**.

Un **callback** es una función que entregas a otra pieza (una fuente, una librería, un framework) para que **ella** la invoque más tarde.

<div class="flow">
  <div><b>Tu código</b><small>define la reacción: { pulsaciones++ }</small></div>
  <div><b>1 · registra</b><small>entrega la función como un valor</small></div>
  <div><b>Fuente</b><small>la guarda: listener = f</small></div>
  <div><b>2 · ocurre el suceso</b><small>clic, timeout, dato recibido…</small></div>
  <div><b>3 · invoca</b><small>la fuente llama a f</small></div>
</div>

Entre el paso 1 y el 3 tu código **no espera**: sigue con otra cosa o incluso termina.

### Tres ideas que lo definen

<div class="cards">
  <div class="card"><h4>1 · Se entrega, no se ejecuta</h4><p>Pasas la función como un valor más. Los paréntesis de la llamada los pondrá otro.</p></div>
  <div class="card"><h4>2 · Inversión de control</h4><p>Tú decides QUÉ ocurre; el que guarda el callback decide CUÁNDO (y cuántas veces, quizá ninguna).</p></div>
  <div class="card"><h4>3 · Un contrato de por medio</h4><p>Ambos lados acuerdan la firma: qué parámetros recibe el callback y qué devuelve.</p></div>
</div>

![Dos carriles en el tiempo. En el de tu código: 1, registra con addListener(f) y sigue con otra cosa o incluso termina. En el de la fuente: guarda f (listener = f), 2, ocurre el suceso (clic, timeout) y 3, invoca f(evento). Una flecha naranja sube de la fuente a tu carril: f se ejecuta (pulsaciones++) porque la fuente la llama. Tú decides qué hace f; quien guarda f decide cuándo.](/img/pde/tema-2/2a-inversion-control.svg "Inversión de control: tu código entrega f y sigue; la fuente la invoca cuando ocurre el suceso")

> [!ANALOGIA] «No nos llame, ya le llamaremos»
> Dejas tu teléfono en la consulta del médico. Tú decides qué harás cuando suene (ir corriendo); la consulta decide cuándo llamarte. El número de teléfono es el callback; el acuerdo «te llamaremos a este número para darte cita» es el contrato.

> [!NOTA] Conexión con la U1
> El **receptor** del trío fuente / evento / receptor se entrega a la fuente precisamente en forma de callback: es el pegamento que une las tres piezas.

**Preguntas**

**1.** En un callback, ¿quién decide **cuándo** se ejecuta la reacción?

   a) El código que define la reacción, al pasarla como argumento
   b) El compilador, que reordena las llamadas
   c) Quien guarda el callback (fuente, librería o framework)
   d) El sistema operativo, siempre en otro hilo

**2.** ¿Cómo se llama el principio por el que tú decides qué pasa, pero otro decide cuándo pasa?

### Tu primer callback: descargar() · Ejercicio

Un ejemplo de consola, sin Android ni GUI, para ver las piezas desnudas:

```kotlin
// Quien OFRECE el servicio declara qué función espera
fun descargar(url: String, alTerminar: (String) -> Unit) {
    val contenido = "…datos de $url…"   // trabajo (simulado)
    alTerminar(contenido)                // «ya te llamo yo»
}

// Quien USA el servicio entrega la función; no la ejecuta
fun main() {
    descargar("https://uax.com") { texto ->
        println("Recibido: $texto")
    }
}
```

Fíjate en tres cosas:

1. **El contrato es un tipo función.** `(String) -> Unit` significa «recibe un `String` y no devuelve nada». Cualquier función con esa forma sirve.
2. **La invocación la hace el otro.** `descargar()` decide cuándo llamar a `alTerminar`, con qué dato… y si llega a llamarlo.
3. **Entregamos código, no un resultado.** La lambda `{ texto -> … }` viaja como argumento. `main()` no sabe en qué momento se ejecutará.

> [!TRUCO] Sintaxis de lambda final
> En Kotlin, si el último parámetro es una función, la lambda puede ir **fuera** de los paréntesis. Por eso `descargar("https://uax.com") { … }` equivale a `descargar("https://uax.com", { … })`.

Responde antes de seguir: lee el código como si fueras `descargar()` y no `main()`.

**Preguntas**

**3.** En `descargar("https://uax.com") { texto -> println("Recibido: $texto") }`, la lambda…

   a) Viaja como argumento y la ejecuta `descargar()` cuando decide
   b) Se ejecuta antes de llamar a `descargar()` y se le pasa su resultado
   c) Se lanza automáticamente en otro hilo
   d) No se ejecuta nunca porque no lleva paréntesis

**4.** ¿Qué decide `descargar()` y **no** `main()`? Marca todas.

   a) Cuándo llamar a `alTerminar`
   b) Qué hace la reacción con el texto recibido
   c) Con qué dato lo llama
   d) Si llega a llamarlo o no

**5.** Escribe el tipo función de Kotlin que significa «recibe un `Int` y no devuelve nada».

### Cuatro formas, un mismo contrato · Teoría

El contrato clásico es una **interfaz de un solo método**. Es lo que hay debajo de `View.OnClickListener` en Android:

```kotlin
// El contrato
fun interface OnPulsado {
    fun pulsado(id: Int)
}

// La fuente guarda el callback
class Boton(val id: Int) {
    var listener: OnPulsado? = null
    fun pulsar() {
        listener?.pulsado(id)
    }
}
```

El compilador acepta **cualquier cosa que cumpla el contrato**. Hay cuatro formas de fabricarla:

```kotlin
// A · Clase con nombre
class Registrador : OnPulsado {
    override fun pulsado(id: Int) = println("Botón $id")
}
boton.listener = Registrador()

// B · Clase anónima
boton.listener = object : OnPulsado {
    override fun pulsado(id: Int) { println("Botón $id") }
}

// C · Lambda (conversión SAM)
boton.listener = OnPulsado { id -> println("Botón $id") }

// D · Referencia a función
fun registrar(id: Int) = println("Botón $id")
boton.listener = OnPulsado(::registrar)
```

| Forma | Úsala cuando… | Cuidado con… |
| --- | --- | --- |
| A · Clase con nombre | La reacción tiene estado propio, se reutiliza o se quiere probar aparte. | Mucho código para reacciones de una línea. |
| B · Clase anónima | La interfaz tiene varios métodos o necesitas `this` del propio listener. | Verbosa; captura la clase que la rodea. |
| C · Lambda | Reacción corta y local. La opción por defecto en Kotlin. | Cada lambda es un objeto distinto (importa al desregistrar). |
| D · Referencia `::f` | La función ya existe y su firma encaja con el contrato. | Una referencia ligada (`obj::f`) retiene a `obj`. |

> [!NOTA] ¿Qué es SAM?
> *Single Abstract Method*: una interfaz con un único método abstracto. Marcarla como `fun interface` permite que una lambda se convierta automáticamente en un objeto que la implementa.

**Preguntas**

**6.** ¿Cómo se llama (por sus siglas) la conversión que permite escribir `OnPulsado { id -> … }` con una lambda?

**7.** Reacción de una línea, local, que no vas a reutilizar. ¿Qué forma es la opción por defecto en Kotlin?

   a) A · Clase con nombre
   b) B · Clase anónima
   c) D · Referencia `::f`
   d) C · Lambda

### Elige la forma adecuada · Ejercicio

Las cuatro formas compilan y hacen lo mismo. Lo que cambia es **cuánto cuesta escribirlas, leerlas y mantenerlas**. Decide para cada situación.

**Situación 1.** Tienes un `AnimatorListener` con cuatro métodos (`onAnimationStart`, `onAnimationEnd`, `onAnimationCancel`, `onAnimationRepeat`) y en uno de ellos necesitas referirte al propio listener con `this` para quitarlo.

**Situación 2.** Ya existe en tu clase `fun guardarEnLog(id: Int)`, que hace justo lo que quieres al pulsar.

**Situación 3.** La reacción lleva un contador interno de pulsaciones y un umbral, la usan tres pantallas y quieres probarla con un test unitario sin botones.

**Situación 4.** Te piden revisar este código:

```kotlin
class Pantalla {
    val datos = cargarImagenesEnormes()
    fun alPulsar(id: Int) { /* … */ }
}

val p = Pantalla()
botonGlobal.listener = OnPulsado(p::alPulsar)
```

`botonGlobal` vive toda la aplicación. ¿Qué consecuencia tiene esa referencia ligada?

> [!TRUCO] Pregunta guía
> Antes de elegir, pregúntate: ¿la reacción tiene **estado** o **varios métodos**? ¿Existe **ya** la función? Si la respuesta es «no» a todo, una lambda.

**Preguntas**

**8.** Situación 1 (interfaz de varios métodos y necesitas `this` del listener):

   a) C · Lambda
   b) B · Clase anónima
   c) D · Referencia `::f`
   d) Ninguna sirve

**9.** Situación 2 (la función ya existe y su firma encaja):

   a) D · Referencia `::guardarEnLog`
   b) A · Clase con nombre
   c) B · Clase anónima
   d) C · Lambda que la llama

**10.** Situación 4: ¿qué pasa con `p` y sus imágenes?

   a) Nada: las referencias a función no retienen objetos
   b) Se copian las imágenes dentro del botón
   c) Siguen en memoria mientras `botonGlobal` guarde el listener, porque `p::alPulsar` retiene a `p`
   d) Se liberan en cuanto termina la función que creó `p`

### Clausuras: la lambda recuerda dónde nació · Teoría

Una lambda puede usar variables del sitio donde se escribió, aunque se ejecute mucho después y desde otro sitio:

```kotlin
fun main() {
    var pulsaciones = 0          // local de main()
    val boton = Boton(1)

    boton.listener = OnPulsado { // captura pulsaciones
        pulsaciones++
        println("Van $pulsaciones")
    }

    boton.pulsar(); boton.pulsar()
}
```

`Boton.pulsar()` no tiene ni idea de que existe `pulsaciones`… pero la lambda sí. Eso es una **clausura** (*closure*): **función + entorno**.

<div class="cards">
  <div class="card"><h4>1 · Función + entorno</h4><p>La lambda recuerda las variables del lugar donde nació, no del lugar donde la llaman.</p></div>
  <div class="card"><h4>2 · Captura la variable, no una copia</h4><p>Si la lambda incrementa <code>pulsaciones</code>, el cambio se ve fuera. En Java solo se pueden capturar variables <b>efectivamente finales</b>.</p></div>
  <div class="card"><h4>3 · Vive lo que viva el callback</h4><p>Mientras el botón guarde la lambda, todo lo que esta referencia sigue en memoria.</p></div>
</div>

> [!CUIDADO] Para la Clase 2
> Si lo capturado es **una pantalla entera** y la fuente nunca suelta el callback… ¿quién libera esa pantalla? Guarda esta pregunta: es el problema del *listener olvidado*.

> [!EXAMEN]
> «Una clausura captura la variable, no su valor» y «lo capturado vive lo que viva el callback» son dos frases que conviene saber justificar con un ejemplo.

**Preguntas**

**11.** ¿Qué imprime el programa? Escribe las dos líneas separadas por una coma.

**12.** ¿Qué captura exactamente la lambda?

   a) Una copia del valor 0 en el momento de crearse
   b) Nada: `pulsar()` le pasa el valor como parámetro
   c) La propia variable `pulsaciones`, así que sus cambios se ven fuera
   d) Solo el objeto `boton`

### Delegación: la pantalla firma el contrato · Teoría

Otra forma muy común en Android (y en Java en general): **la propia clase implementa la interfaz del listener** y se entrega a sí misma con `this`.

```kotlin
class PantallaLogin : AppCompatActivity(), View.OnClickListener {

    override fun onCreate(b: Bundle?) {
        super.onCreate(b)
        setContentView(R.layout.login)
        findViewById<Button>(R.id.btnEntrar).setOnClickListener(this)
        findViewById<Button>(R.id.btnSalir).setOnClickListener(this)
    }

    override fun onClick(v: View) {
        when (v.id) {
            R.id.btnEntrar -> entrar()
            R.id.btnSalir  -> finish()
        }
    }
}
```

1. **La pantalla firma el contrato.** Al implementar `OnClickListener`, la Activity se convierte en un callback válido.
2. **Dos fuentes, un mismo delegado.** Cada botón solo sabe *a quién avisar* (`this`). El botón sigue siendo reutilizable: no sabe nada del login.
3. **El delegado reparte.** `when (v.id)` asocia *fuente → reacción*. Es, literalmente, una **tabla de despacho escrita a mano**.

> [!NOTA] Delegar = encargar a otro
> La fuente (el botón) **delega** la decisión de qué hacer en otro objeto (la pantalla). Por eso se habla del patrón *delegación*: el botón hace de botón y la pantalla decide.

> [!CUIDADO] Lo que viene
> Cada botón nuevo añade una rama al `when`. Con tres no pasa nada; con treinta, ese `when` se convierte en un sitio donde todo el mundo toca. En la Clase 2 lo cambiaremos por **datos** (una tabla) en vez de **código**.

**Preguntas**

**13.** El usuario pulsa **Salir**. ¿Qué secuencia ocurre?

   a) El botón llama a `finish()` directamente
   b) El botón llama a `onClick(v)` de la pantalla con `v.id == R.id.btnSalir`, y el `when` elige `finish()`
   c) Android llama a `onCreate` de nuevo y registra otra vez los botones
   d) El botón llama a `entrar()` y luego a `finish()`

**14.** ¿Qué es `when (v.id)` dentro de `onClick`, en el vocabulario de la unidad?

   a) Una tabla de despacho escrita a mano (fuente → reacción)
   b) Un callback diferido
   c) Una clausura
   d) Un listener por botón

### ¿Ahora o después? Síncrono frente a diferido · Teoría

La sintaxis de entregar un callback es **idéntica** en `lista.forEach { … }` y en `boton.setOnClickListener { … }`. Lo que cambia es **qué hace con él quien lo recibe**.

<div class="cards">
  <div class="card"><h4>Síncrono · forEach { … }</h4><p>Se invoca <b>antes de que retorne</b> la función que lo recibe, en la misma pila y el mismo hilo. Ej.: <code>forEach</code>, <code>map</code>, <code>sortedBy</code>, el <code>descargar()</code> de antes.</p></div>
  <div class="card"><h4>Diferido / asíncrono · setOnClickListener { … }</h4><p>La función solo lo <b>guarda</b> y retorna. Lo invoca más tarde el bucle de eventos u otro hilo: 0, 1 o N veces. Ej.: listeners de UI, temporizadores, red, sensores.</p></div>
</div>

![Dos pilas de llamadas sobre una línea de tiempo. Síncrono, lista.forEach: dentro de la barra del llamante está forEach y, dentro de forEach, dos ejecuciones del callback; forEach retorna después; misma pila, mismo hilo. Diferido, boton.setOnClickListener: dentro del llamante, la llamada solo guarda f y retorna enseguida; el llamante termina. Más tarde, en otra pila (el bucle de eventos u otro hilo), se ejecuta el callback tras el clic.](/img/pde/tema-2/2a-sincrono-diferido.svg "Misma sintaxis, distinto momento: el síncrono corre dentro de la llamada; el diferido, en otra pila más tarde")

| ¿Cuándo corre? | Callback síncrono | Callback diferido / asíncrono |
| --- | --- | --- |
| Momento | Antes de que retorne la función que lo recibe. | Después: cuando ocurra el suceso (o nunca). |
| Pila de llamadas | La del llamante: en el depurador ves quién te llamó. | La del bucle de eventos o la de otro hilo. |
| Excepciones | Un `try/catch` alrededor de la llamada las captura. | El `try/catch` del registro no las ve: hay que tratarlas dentro. |
| Hilo | El mismo que hizo la llamada. | Puede ser otro (hilo de UI, del timer, de red). |
| Estado | No cambia entre el registro y la ejecución. | Puede haber cambiado: compruébalo de nuevo al entrar. |
| Vida de lo capturado | Dura lo que la llamada. | Dura mientras la fuente guarde el callback. |

> [!IMPORTANTE] Diferido no significa necesariamente otro hilo
> El **mismo** bucle de eventos puede ejecutarlo más tarde. En JavaScript lo ves claro:

```js
function aplicar(x, cb) { cb(x); }
aplicar(5, console.log);               // síncrono: imprime ya
setTimeout(() => console.log(5), 0);   // diferido: queda programado
```

La primera reacción se ejecuta antes de que `aplicar` retorne; la segunda queda en la cola y la ejecutará el mismo hilo **cuando termine el código actual**, aunque el retardo sea 0.

**Preguntas**

**15.** ¿Qué llamadas invocan su callback **de forma síncrona** (antes de retornar)?

   a) `lista.forEach { … }`
   b) `boton.setOnClickListener { … }`
   c) `lista.sortedBy { … }`
   d) `setTimeout(() => …, 0)`
   e) El `descargar(url) { … }` de antes
   f) `lista.map { … }`

**16.** Envuelves `boton.setOnClickListener { error("fallo") }` en un `try/catch`. Al pulsar el botón, ¿quién captura la excepción?

   a) Tu `try/catch`, porque rodea el código de la lambda
   b) Nadie de tu `try/catch`: cuando se lanza, el registro ya terminó hace rato
   c) El compilador la convierte en un aviso
   d) La propia lambda, automáticamente

**17.** Ordena lo que imprime: `aplicar(5, console.log); setTimeout(() => console.log("T"), 0); console.log("fin");`

   Ordena: 5 · T · fin

### Predice: A, B, C, D con un Timer · Ejercicio

Dos callbacks con la misma pinta. Predice la salida **antes** de ejecutar (en clase: 1 minuto en pareja).

```kotlin
import java.util.Timer
import kotlin.concurrent.schedule

fun main() {
    println("A")
    listOf(1, 2).forEach { println("B$it") }
    Timer().schedule(1000) { println("C") }
    println("D")
}
```

Piénsalo así:

1. ¿Qué llamadas ejecutan el callback **antes de retornar**?
2. ¿Cuáles solo lo **guardan** para más tarde?
3. Cuando por fin se ejecute «C», ¿seguirá `main()` en marcha? ¿En qué hilo correrá?

> [!TRUCO] Pista
> Que dos líneas se parezcan no significa que se ejecuten igual: hay que saber qué hace con el callback **quien lo recibe**.

Cuando tengas tu predicción, compruébala en la línea temporal:

> _Aquí hay un simulador interactivo (`sync-async`): ábrelo en la web._
> [!NOTA] Un detalle de la JVM
> `java.util.Timer` crea su propio hilo (`Timer-0`) y, por defecto, no es *daemon*: el proceso no termina al acabar `main()` mientras ese hilo siga vivo. Por eso «C» llega a imprimirse aunque `main()` ya haya retornado.

> [!EXAMEN]
> Este tipo de ejercicio («¿en qué orden se imprime?») es carne de parcial. La clave siempre es la misma: **¿la función que recibe el callback lo ejecuta o lo guarda?**

**Preguntas**

**18.** Ordena las líneas tal como aparecen en la consola.

   Ordena: A · B1 · B2 · C · D

**19.** ¿En qué hilo se ejecuta `println("C")`?

   a) En `main`, cuando termina
   b) En el hilo de UI
   c) En el hilo del Timer (`Timer-0`)
   d) En ninguno: no llega a ejecutarse

**20.** Cuando se imprime «C», ¿sigue `main()` en marcha?

   a) Sí, `main()` espera bloqueada en `schedule` hasta que pase el segundo
   b) No: `main()` ya retornó; la lambda sigue viva porque el `Timer` la guarda
   c) Sí, porque `println("D")` tarda un segundo
   d) No, y por eso C no se imprime nunca

### Listener frente a handler · Teoría

Dos palabras que se usan casi como sinónimos, pero que responden a preguntas de diseño distintas.

<div class="cards">
  <div class="card"><h4>LISTENER · «el que escucha»</h4><p>Se <b>suscribe a una fuente concreta</b> y es avisado cuando algo ocurre. De 0 a N por fuente; la fuente no sabe quiénes son ni qué hacen. Se registra y se da de baja en ejecución: <code>addXxxListener</code> / <code>removeXxxListener</code>.<br><br>Ejemplos: <code>OnClickListener</code>, <code>SensorEventListener</code>, el consumidor de una cola.</p></div>
  <div class="card"><h4>HANDLER · «el que se hace cargo»</h4><p>Es el <b>responsable de un tipo de evento</b>: el código al que el despacho entrega ese caso. Normalmente uno por tipo; lo elige el despachador según el evento. Puede <b>consumir</b> el evento e impedir que siga propagándose (p. ej. devolver <code>true</code> en <code>onTouch</code>).<br><br>Ejemplos: <code>manejar()</code> de agendaeventos.kt, cada rama de un <code>when</code>, una rutina de interrupción.</p></div>
</div>

La pregunta que te tienes que hacer al diseñar:

> **¿Se suscribe a una fuente (listener) o atiende un tipo de evento (handler)?**

Dicho de otra forma, como lo resumen los obligatorios de la unidad: *suscripción = quién recibe; handler = qué hace al recibir*. El listener expresa un **interés continuado** (puede recibir muchas notificaciones); el handler contiene la reacción que procesa **un evento concreto**.

> [!CUIDADO] Ojo en Android
> `android.os.Handler` es **otra cosa**: es la puerta a la cola de mensajes de un hilo (sirve para mandarle trabajo). No confundas la clase de Android con el concepto general de handler.

**Preguntas**

**21.** Una rutina de interrupción que atiende el evento «byte recibido por la UART» es, en este vocabulario…

   a) Un listener
   b) Un handler
   c) Una fuente
   d) Una clausura

**22.** ¿Qué es `android.os.Handler`?

   a) El nombre que da Android a cualquier listener
   b) La interfaz que implementa `onClick`
   c) Una clase ligada a la cola de mensajes de un hilo
   d) El despachador de eventos táctiles

### Una fuente por dentro: lista, alta y baja · Teoría

¿Cómo está hecha por dentro una fuente que admite varios listeners? Una lista y tres operaciones:

```kotlin
typealias Listener = (String) -> Unit

class Sensor {
    private val listeners = mutableListOf<Listener>()

    fun addListener(l: Listener)    { listeners += l }
    fun removeListener(l: Listener) { listeners -= l }

    fun publicar(dato: String) {
        for (l in listeners.toList()) l(dato)
    }
}
```

![A la izquierda, desde fuera, addListener(l) y removeListener(l) apuntan a la lista; la baja exige la misma referencia. En el centro, el Sensor con su lista privada listeners: enPantalla, enFichero y alarma. A la derecha, publicar(dato) recorre una copia discontinua de esa lista y llama l(dato) a cada uno, uno detrás de otro.](/img/pde/tema-2/2a-fuente-lista.svg "Una fuente por dentro: lista privada, alta y baja desde fuera, y difusión sobre una copia")

<div class="cards">
  <div class="card"><h4>1 · La lista es de la fuente</h4><p>Es <b>privada</b>: desde fuera solo se puede pedir alta o baja. Cuándo se recorre lo decide la fuente.</p></div>
  <div class="card"><h4>2 · Se recorre una copia</h4><p><code>toList()</code> hace una copia. Así un listener puede darse de baja (o añadir otro) dentro de su propio callback sin romper la iteración.</p></div>
  <div class="card"><h4>3 · La baja exige la misma referencia</h4><p>Dos lambdas con el mismo texto son <b>dos objetos</b>. Guarda el listener en una <code>val</code> si piensas quitarlo.</p></div>
</div>

```kotlin
val enPantalla: Listener = { println("UI: $it") }

sensor.addListener(enPantalla)
sensor.removeListener(enPantalla)               // OK
sensor.removeListener { println("UI: $it") }    // no quita nada
```

> [!NOTA] ¿Por qué la copia?
> Si recorres la lista original y, dentro del bucle, un listener se da de baja a sí mismo, estás modificando la colección que iteras. En Kotlin/Java eso suele acabar en `ConcurrentModificationException`. Recorrer una **foto** de la lista evita el problema: los cambios se aplican a la siguiente ronda.

**Preguntas**

**23.** ¿Para qué sirve el `toList()` en `publicar()`?

   a) Para que los listeners se ejecuten en paralelo
   b) Para ordenar los listeners por prioridad
   c) Para que un listener pueda darse de alta o de baja durante la difusión sin romper la iteración
   d) Para convertir el `String` en una lista de caracteres

**24.** ¿Por qué `listeners` es `private`?

   a) Para que solo la fuente decida cuándo se recorre y desde fuera solo se pueda pedir alta o baja
   b) Porque Kotlin no permite listas públicas de funciones
   c) Para que la recolección de basura la libere antes
   d) Para que los listeners no se ejecuten dos veces

### Encuentra el fallo: la baja que no da de baja · Ejercicio

Un compañero escribe esto y jura que ha quitado el listener:

```kotlin
val sensor = Sensor()

sensor.addListener { println("UI: $it") }
sensor.addListener { println("LOG: $it") }

// … más tarde, al cerrar la pantalla
sensor.removeListener { println("UI: $it") }

sensor.publicar("21 ºC")
```

Antes de mirar las preguntas, razona:

- ¿Cuántos objetos función hay en la lista **antes** de `removeListener`?
- ¿Qué objeto le estás pidiendo a la lista que quite?
- ¿Está ese objeto en la lista?

> [!CUIDADO] Igualdad de lambdas
> Dos lambdas con el mismo código fuente son dos instancias distintas. `removeListener` compara por **identidad** del objeto, no por lo que «pone» dentro. Lo mismo pasa en el navegador con `removeEventListener` si le pasas una función flecha nueva.

<details><summary>Ver solución comentada</summary>
<p>La lista tiene dos lambdas. <code>removeListener { … }</code> crea una <b>tercera</b> lambda, que no está en la lista, así que <code>-=</code> no encuentra nada que quitar. Al publicar se imprimen <code>UI: 21 ºC</code> y <code>LOG: 21 ºC</code>. Arreglo: guardar el listener en una <code>val</code> y usar esa misma referencia en el alta y en la baja (o, como veremos en la Clase 2, que el alta devuelva una suscripción cancelable).</p>
</details>

**Preguntas**

**25.** ¿Qué imprime `sensor.publicar("21 ºC")`?

   a) Solo `LOG: 21 ºC`
   b) `UI: 21 ºC` y `LOG: 21 ºC`
   c) Nada: `removeListener` vació la lista
   d) Lanza una excepción porque el listener no existe

**26.** ¿Qué cambios arreglan la baja? Marca todos los válidos.

   a) Guardar la lambda en `val enPantalla` y usar esa misma `val` en `addListener` y `removeListener`
   b) Llamar a `removeListener { … }` dos veces para asegurarse
   c) Hacer que `addListener` devuelva un objeto/función que, al invocarlo, quite exactamente ese listener
   d) Escribir la lambda de la baja exactamente igual, con los mismos espacios

### Varios suscriptores en una misma fuente · Teoría

Una misma fuente, un mismo evento, N reacciones independientes:

<div class="flow">
  <div><b>Sensor</b><small>publicar("21 ºC")</small></div>
  <div><b>1 · enPantalla</b><small>pinta el valor</small></div>
  <div><b>2 · enFichero</b><small>lo guarda en el log</small></div>
  <div><b>3 · alarma</b><small>avisa si supera el umbral</small></div>
</div>

Es una **difusión** (*broadcast*): cada listener recibe su propia llamada con el mismo evento, una detrás de otra. Cuatro reglas:

1. **El orden habitual es el de alta…** pero solo es un contrato si la documentación lo dice. El DOM lo garantiza; Swing, no.
2. **No dependas del orden.** Si B necesita que A haya corrido antes, no son dos listeners: es uno solo, o A debe emitir su propio evento.
3. **Una excepción corta la cadena.** Si el 2 falla, el 3 no se entera. Una fuente robusta envuelve cada llamada en `try/catch`.
4. **Consumir el evento.** Algunos despachos permiten parar la propagación: devolver `true`, `stopPropagation()`…

```kotlin
fun publicar(dato: String) {
    for (l in listeners.toList()) {
        try { l(dato) }
        catch (e: Exception) { println("listener falló: ${e.message}") }
    }
}
```

> [!IMPORTANTE] Contrato de notificación
> Una fuente con varios receptores debe dejar claro: si el fallo de un handler **detiene o no** a los siguientes, si una alta o baja durante la notificación **afecta a la ronda actual**, y qué pasa con una entrega **ya encolada** cuando alguien se da de baja.

**Preguntas**

**27.** Con el `publicar()` original (sin `try/catch`), `enFichero` lanza una excepción. ¿Qué le pasa a `alarma`?

   a) Se ejecuta igualmente, después del error
   b) Se ejecuta antes que `enFichero` para evitar el fallo
   c) No se entera: la excepción corta el bucle de difusión
   d) Se ejecuta dos veces para compensar

**28.** ¿En cuál de estas plataformas está documentado que los listeners se llaman en orden de alta?

   a) Swing
   b) El DOM del navegador
   c) En todas: es una ley general
   d) En ninguna

**29.** El listener B solo funciona si A ya se ha ejecutado. ¿Qué haces?

   a) Registrar A antes que B y confiar en el orden
   b) Fusionarlos en uno solo, o que A emita su propio evento al que se suscriba B
   c) Registrar B dos veces
   d) Añadir un `Thread.sleep` en B

### Repaso de la Clase 1 · Repaso

> [!RESUMEN] Chuleta
> 1. **Callback**: función que entregas para que otro la invoque. Tú decides **qué**; él decide **cuándo** (inversión de control). Hay un contrato: la firma.
> 2. **Cuatro sintaxis, un contrato**: clase con nombre, clase anónima, lambda (SAM) y referencia `::f` cumplen la misma interfaz de un método.
> 3. **Clausura** = función + entorno. Captura la variable, no una copia; lo capturado vive lo que viva el callback.
> 4. **Síncrono ≠ diferido**: los listeners de eventos son diferidos (otra pila, quizá otro hilo, estado que pudo cambiar). Diferido no implica necesariamente otro hilo.
> 5. **Fuente = lista de listeners**: alta, baja (misma referencia) y difusión sobre una copia. El orden no se presupone; una excepción corta la cadena.

| Concepto | Pregunta que responde |
| --- | --- |
| Listener | ¿Quién se suscribe a esta fuente? |
| Handler | ¿Quién se hace cargo de este tipo de evento? |
| Delegación | ¿A qué objeto avisa la fuente para que decida? |

### Para pensar antes de la Clase 2

1. Una pantalla se registra como listener del GPS y el usuario la cierra. **¿Quién la da de baja?**
2. Nuestro `when (v.id)` crece con cada botón nuevo. ¿Cómo asociar evento → reacción **con datos** en vez de con código?
3. En C no hay lambdas ni clausuras. ¿Cómo le llega a un callback **«su» contexto**?

**Preguntas**

**30.** Pregunta tipo examen: ¿qué diferencia a un callback síncrono de uno diferido?

   a) El síncrono se escribe con lambda y el diferido con clase anónima
   b) El síncrono se invoca antes de que retorne la función que lo recibe; el diferido se guarda y se invoca después (o nunca)
   c) El diferido siempre se ejecuta en otro hilo
   d) El síncrono solo existe en JavaScript

**31.** ¿Qué necesita `removeListener` para quitar de verdad un listener? (dos o tres palabras)

**32.** Marca las afirmaciones **verdaderas**.

   a) Una referencia ligada `obj::f` retiene a `obj` mientras la fuente la guarde
   b) Un `try/catch` alrededor de `setOnClickListener` captura las excepciones del clic
   c) En Java, una lambda solo puede capturar variables efectivamente finales
   d) Si un listener depende de que otro se ejecute antes, basta con registrarlo después
   e) `when (v.id)` en un `onClick` compartido es una tabla de despacho escrita a mano

### Solucionario

1. c) Quien guarda el callback (fuente, librería o framework)  
   Tú decides **qué** ocurre; quien guarda el callback decide **cuándo** y cuántas veces lo invoca. Eso es la inversión de control.
2. `inversion de control`  
   **Inversión de control**: el flujo ya no lo dirige tu código, sino quien guarda tus callbacks.
3. a) Viaja como argumento y la ejecuta `descargar()` cuando decide  
   La lambda es un **valor**. Quien pone los paréntesis de la llamada es `descargar()`, en la línea `alTerminar(contenido)`.
4. a) Cuándo llamar a `alTerminar`; c) Con qué dato lo llama; d) Si llega a llamarlo o no  
   Qué hacer con el texto lo decide quien escribe la lambda (`main`). Cuándo, con qué dato y si se llama, lo decide `descargar()`.
5. `(Int) -> Unit`  
   `(Int) -> Unit`. `Unit` es el «no devuelve nada» de Kotlin (el `void` de Java o C).
6. `SAM`  
   **Conversión SAM**: una interfaz con un único método abstracto (`fun interface`) se puede implementar con una lambda.
7. d) C · Lambda  
   La **lambda** es la forma corta y local. Ojo solo a una cosa: cada lambda es un objeto distinto, lo que importará al darse de baja.
8. b) B · Clase anónima  
   Con varios métodos no hay conversión SAM, y dentro de un `object : …` el `this` es el propio listener. **Clase anónima**.
9. a) D · Referencia `::guardarEnLog`  
   Si la función ya existe con la firma del contrato, la **referencia** `::f` evita escribir una lambda envoltorio.
10. c) Siguen en memoria mientras `botonGlobal` guarde el listener, porque `p::alPulsar` retiene a `p`  
   Una **referencia ligada** `obj::f` guarda dentro a `obj`. Mientras la fuente la guarde, `p` (y sus imágenes) no se puede liberar. Es el germen de las fugas de la Clase 2.
11. `Van 1, Van 2`  
   `Van 1` y `Van 2`: la lambda incrementa **la misma** variable `pulsaciones` en cada llamada.
12. c) La propia variable `pulsaciones`, así que sus cambios se ven fuera  
   Kotlin captura **la variable**. Por eso el contador avanza. Java, en cambio, solo te deja capturar variables efectivamente finales (que no cambian).
13. b) El botón llama a `onClick(v)` de la pantalla con `v.id == R.id.btnSalir`, y el `when` elige `finish()`  
   El botón invoca el **delegado** (`onClick`) y es el delegado quien despacha según `v.id`.
14. a) Una tabla de despacho escrita a mano (fuente → reacción)  
   Asocia cada fuente con su reacción: una **tabla de despacho** hecha con código. En la Clase 2 la convertiremos en datos.
15. a) `lista.forEach { … }`; c) `lista.sortedBy { … }`; e) El `descargar(url) { … }` de antes; f) `lista.map { … }`  
   `forEach`, `sortedBy`, `map` y nuestro `descargar()` llaman al callback antes de volver. Los listeners de UI y los temporizadores solo lo **guardan**.
16. b) Nadie de tu `try/catch`: cuando se lanza, el registro ya terminó hace rato  
   El callback es **diferido**: se ejecuta en otra pila (la del bucle de eventos). El `try/catch` del registro ya no existe. Hay que tratar las excepciones **dentro** del callback.
17. 1. 5 → 2. fin → 3. T  
   `aplicar` llama al callback antes de retornar (5). `setTimeout` solo programa; se imprime `fin` y, cuando el código actual termina, el bucle ejecuta el callback (T).
18. 1. A → 2. B1 → 3. B2 → 4. D → 5. C  
   `forEach` ejecuta su lambda antes de retornar (B1, B2). `schedule` la guarda y retorna enseguida, así que se imprime D. Un segundo después, el hilo del Timer imprime C.
19. c) En el hilo del Timer (`Timer-0`)  
   El `Timer` tiene su propio hilo. Diferido aquí sí implica otro hilo (aunque no siempre es así).
20. b) No: `main()` ya retornó; la lambda sigue viva porque el `Timer` la guarda  
   `main()` terminó tras imprimir D. La lambda sobrevive porque la guarda el `Timer`: lo capturado vive lo que viva el callback.
21. b) Un handler  
   Se hace cargo de **un tipo de evento** concreto: es un **handler**.
22. c) Una clase ligada a la cola de mensajes de un hilo  
   Comparte nombre, pero es otra cosa: la vía para encolar mensajes o tareas en la cola de un hilo.
23. c) Para que un listener pueda darse de alta o de baja durante la difusión sin romper la iteración  
   Se recorre una **copia estable**: las altas y bajas que ocurran durante la ronda modifican la lista real, no la que se está recorriendo.
24. a) Para que solo la fuente decida cuándo se recorre y desde fuera solo se pueda pedir alta o baja  
   La lista es **de la fuente**. Si fuese pública, cualquiera podría vaciarla, reordenarla o invocar a los listeners cuando quisiera.
25. b) `UI: 21 ºC` y `LOG: 21 ºC`  
   La baja no quitó nada: se le pasó un objeto nuevo. Los dos listeners siguen ahí. Y `-=` sobre una lista no falla si el elemento no está.
26. a) Guardar la lambda en `val enPantalla` y usar esa misma `val` en `addListener` y `removeListener`; c) Hacer que `addListener` devuelva un objeto/función que, al invocarlo, quite exactamente ese listener  
   Solo funciona usar **la misma referencia**, o que la fuente te devuelva una **suscripción cancelable** que ya la recuerda por ti.
27. c) No se entera: la excepción corta el bucle de difusión  
   La excepción sale del bucle `for` y los listeners posteriores no se llaman. Por eso una fuente robusta envuelve **cada** llamada.
28. b) El DOM del navegador  
   El DOM lo garantiza; Swing no. Por eso la regla es: **no presupongas el orden** salvo que la documentación lo prometa.
29. b) Fusionarlos en uno solo, o que A emita su propio evento al que se suscriba B  
   Si hay dependencia, **no son dos listeners independientes**: o son uno, o A publica un evento propio («ya he terminado») al que escucha B.
30. b) El síncrono se invoca antes de que retorne la función que lo recibe; el diferido se guarda y se invoca después (o nunca)  
   La diferencia está en **quién recibe el callback y qué hace con él**: ejecutarlo ya o guardarlo. La sintaxis es idéntica, y diferido no implica otro hilo.
31. `la misma referencia`  
   **La misma referencia** que se usó en el alta. Dos lambdas iguales en texto son objetos distintos.
32. a) Una referencia ligada `obj::f` retiene a `obj` mientras la fuente la guarde; c) En Java, una lambda solo puede capturar variables efectivamente finales; e) `when (v.id)` en un `onClick` compartido es una tabla de despacho escrita a mano  
   El `try/catch` del registro no ve excepciones diferidas, y depender del orden de alta es frágil salvo que la plataforma lo garantice.

## Ciclo de vida y despacho

_Quién da de baja a un listener y por qué olvidarlo fuga memoria; el mismo concepto de referencia a función en seis lenguajes; tablas de despacho en C, Kotlin y JS, y el contexto (void *ctx) que acompaña a cada callback._

### El listener olvidado · Teoría

Repaso exprés de la Clase 1:

<div class="cards">
  <div class="card"><h4>Callback</h4><p>Función que entregas para que otro la invoque. Tú decides QUÉ; quien la guarda decide CUÁNDO. Si es diferido: otra pila, quizá otro hilo.</p></div>
  <div class="card"><h4>Listener</h4><p>Callback suscrito a una FUENTE. De 0 a N por fuente, con alta y baja en ejecución. La baja exige la misma referencia.</p></div>
  <div class="card"><h4>Handler</h4><p>Callback responsable de un TIPO de evento. Lo elige el despacho. Hasta ahora, escrito a mano: <code>when (evento) { … }</code>.</p></div>
</div>

Hoy: **quién es el dueño de cada callback y cuánto debe vivir**, y cómo montar el despacho para que sirva igual en Android, en un servidor o en un ESP32.

### Quien guarda un callback mantiene vivo todo lo que ese callback referencia

<div class="flow">
  <div><b>Gps</b><small>fuente · vive lo que la app</small></div>
  <div><b>listeners</b><small>lista de callbacks</small></div>
  <div><b>{ pos -> pintar(pos) }</b><small>la lambda captura this</small></div>
  <div><b>PantallaMapa</b><small>debería vivir lo que la pantalla</small></div>
</div>

Cada flecha es una **referencia fuerte**: mientras exista la primera caja, existe la última. El recolector de basura no puede liberar nada de lo que todavía es alcanzable desde el GPS.

![Arriba, la cadena de referencias fuertes: Gps (fuente) apunta a listeners (lista), que apunta a la lambda { pos -> pintar(pos) } (captura this), que apunta a PantallaMapa, ya cerrada. Abajo, los tiempos de vida: Gps vive lo que la app; PantallaMapa está abierta hasta que el usuario la cierra y, a partir de ahí, sigue en memoria.](/img/pde/tema-2/2a-listener-olvidado.svg "El listener olvidado: la pantalla cerrada sigue alcanzable desde el GPS")

<div class="cards">
  <div class="card"><h4>Síntoma 1 · Fuga de memoria</h4><p>El usuario cierra la pantalla, pero el GPS sigue apuntando a ella: ni la pantalla ni sus vistas pueden liberarse. Diez aperturas = diez pantallas en memoria.</p></div>
  <div class="card"><h4>Síntoma 2 · Callbacks fantasma</h4><p>La fuente sigue invocando al listener de una pantalla que ya no existe: trabajo inútil, estado incoherente y, a menudo, una excepción.</p></div>
</div>

**Preguntas**

**1.** La pantalla se cerró, pero no se libera. ¿Por qué?

   a) Porque Android nunca libera Activities
   b) Porque el GPS guarda la lambda y la lambda captura `this` (la pantalla): hay una cadena de referencias fuertes desde algo que vive toda la app
   c) Porque la lambda se ejecuta en otro hilo
   d) Porque `pintar()` es una función privada

**2.** ¿Qué síntomas produce un listener olvidado?

   a) Fuga de memoria: pantallas cerradas que siguen ocupando memoria
   b) Error de compilación en el `addListener`
   c) Callbacks fantasma: se invoca código de una pantalla que ya no existe
   d) El GPS deja de emitir posiciones

### Regla nº 1: toda alta tiene su baja · Teoría

Se registra y se desregistra en **métodos simétricos del ciclo de vida**, con **la misma referencia**.

```kotlin
class PantallaMapa : AppCompatActivity() {

    private val alMoverse: Listener = { pos ->
        pintar(pos)                         // captura this
    }

    override fun onStart() {                // alta
        super.onStart()
        Gps.addListener(alMoverse)
    }

    override fun onStop() {                 // baja
        super.onStop()
        Gps.removeListener(alMoverse)
    }
}
```

1. **Una sola referencia.** El listener vive en una propiedad: es el mismo objeto en el alta y en la baja.
2. **Alta cuando empiezo a necesitarlo.** `onStart`: la pantalla se hace visible. Antes no tiene sentido recibir posiciones.
3. **Baja en el método simétrico.** `onStart ↔ onStop`, `onCreate ↔ onDestroy`, `onResume ↔ onPause`. **Nunca cruzados.**

### El mismo patrón en la web

```js
class PantallaMapa {
  alMoverse = (pos) => {          // captura this
    this.pintar(pos);
  };
  montar()    { gps.addListener(this.alMoverse); }     // alta
  desmontar() { gps.removeListener(this.alMoverse); }  // baja
}
```

La función flecha en un campo **fija `this`** y es el mismo objeto en el alta y en la baja. `montar` equivale a `onStart`: `connectedCallback` en un Web Component o el efecto en React. `desmontar` es `disconnectedCallback` o la función de limpieza del efecto. Si se olvida, la misma fuga que en Android.

> [!EXAMEN]
> «Alta y baja en métodos simétricos del ciclo de vida, con la misma referencia» es la respuesta corta a «¿cómo evitas un listener olvidado?».

**Preguntas**

**3.** Si das de alta un listener en `onResume`, ¿en qué método lo das de baja?

**4.** En la versión JS, ¿por qué `alMoverse` es un **campo** con función flecha y no se escribe `gps.addListener((p) => this.pintar(p))`?

   a) Porque las funciones flecha son más rápidas
   b) Porque así fija `this` y es la misma referencia en el alta y en la baja
   c) Porque JavaScript no admite lambdas como argumento
   d) Porque así el listener se ejecuta de forma síncrona

**5.** En un componente React que se suscribe al GPS dentro de un efecto, ¿dónde va la baja?

   a) En la función de limpieza que devuelve el efecto
   b) En el constructor
   c) En ningún sitio: React da de baja solo
   d) En el siguiente renderizado

### Revisión de código: tres pantallas · Ejercicio

Haces de revisor. Tres compañeros han escrito su pantalla de mapa. `Gps` es un `object` que vive toda la app.

```kotlin
// Pantalla 1
class P1 : AppCompatActivity() {
    private val alMoverse: Listener = { pos -> pintar(pos) }
    override fun onCreate(b: Bundle?)  { super.onCreate(b); Gps.addListener(alMoverse) }
    override fun onDestroy()           { super.onDestroy(); Gps.removeListener(alMoverse) }
}

// Pantalla 2
class P2 : AppCompatActivity() {
    private val alMoverse: Listener = { pos -> pintar(pos) }
    override fun onStart() { super.onStart(); Gps.addListener(alMoverse) }
    override fun onPause() { super.onPause(); Gps.removeListener(alMoverse) }
}

// Pantalla 3
class P3 : AppCompatActivity() {
    override fun onStart() { super.onStart(); Gps.addListener { pos -> pintar(pos) } }
    override fun onStop()  { super.onStop();  Gps.removeListener { pos -> pintar(pos) } }
}
```

Para cada una pregúntate: ¿alta y baja son **simétricas**? ¿Usan **la misma referencia**?

> [!TRUCO] Recordatorio del ciclo de vida
> Si se abre un diálogo encima de la pantalla, la Activity puede pasar por `onPause` y luego por `onResume` **sin** pasar por `onStop`/`onStart`.

<details><summary>Ver solución comentada</summary>
<p><b>P1</b> es correcta: <code>onCreate ↔ onDestroy</code> y la misma propiedad. (Recibe posiciones también cuando no es visible, pero no fuga.)</p>
<p><b>P2</b> cruza pares: alta en <code>onStart</code>, baja en <code>onPause</code>. Tras un diálogo (<code>onPause → onResume</code>) se queda dada de baja aunque sigue visible, porque <code>onStart</code> no se vuelve a llamar.</p>
<p><b>P3</b> crea una lambda nueva en la baja: no quita nada. Cada apertura deja una lambda (y su pantalla) colgada del GPS.</p>
</details>

**Preguntas**

**6.** ¿Qué pantallas tienen un fallo en la gestión del listener?

   a) P1
   b) P2
   c) P3

**7.** Con P3, el usuario abre y cierra la pantalla diez veces. ¿Qué hay colgado del GPS?

   a) Nada: cada `removeListener` quita la suya
   b) Una sola lambda, la última
   c) Diez lambdas, cada una reteniendo su pantalla: fuga y callbacks fantasma
   d) Veinte lambdas, porque cada baja añade una

### ¿Quién es el dueño? Cuatro estrategias · Teoría

**Ownership**: quién responde de que el callback deje de existir a tiempo.

<div class="cards">
  <div class="card"><h4>1 · Baja explícita</h4><p>El suscriptor se da de baja en el método simétrico. Simple, pero depende de la disciplina de quien programa.</p></div>
  <div class="card"><h4>2 · Suscripción cancelable</h4><p>El alta devuelve un objeto «suscripción». Quien lo guarda es el dueño; cancelarlo es la baja.</p></div>
  <div class="card"><h4>3 · Referencia débil</h4><p>La fuente guarda <code>WeakReference</code>: no impide liberar al listener. La limpieza llega tarde y sin avisar.</p></div>
  <div class="card"><h4>4 · Ligado al ciclo de vida</h4><p>El alta recibe el ciclo de vida del suscriptor y la fuente da de baja sola (<code>LiveData</code>, <code>DisposableEffect</code>).</p></div>
</div>

### Estrategia 2 en Kotlin

```kotlin
fun interface Suscripcion { fun cancelar() }

class Sensor {
    private val listeners = mutableListOf<Listener>()
    fun addListener(l: Listener): Suscripcion {
        listeners += l
        return Suscripcion { listeners -= l }
    }
}

val s = sensor.addListener { println(it) }
// …
s.cancelar()            // baja sin guardar la lambda
```

La suscripción **recuerda por ti** la referencia exacta. Adiós al problema de la lambda distinta.

### Estrategia 2 en JavaScript (la habitual)

```js
class Sensor {
  #listeners = [];
  addListener(l) {
    this.#listeners.push(l);
    return () => this.removeListener(l);   // el alta devuelve la baja
  }
}
const cancelar = sensor.addListener(console.log);
cancelar();
```

En el DOM existe `AbortController`:

```js
const control = new AbortController();
boton.addEventListener("click", alPulsar, { signal: control.signal });
// …
control.abort();   // da de baja todo lo ligado a esa señal
```

**Preguntas**

**8.** ¿Cuál es la pega de la estrategia 3 (referencia débil)?

   a) Impide liberar al listener
   b) La limpieza llega tarde y sin avisar: el listener puede seguir recibiendo eventos hasta que actúe el recolector
   c) Solo funciona en C
   d) Obliga a guardar la lambda en una `val`

**9.** En la estrategia 2, ¿quién es el dueño del callback?

   a) La fuente
   b) El recolector de basura
   c) Quien guarda el objeto suscripción
   d) El hilo principal

**10.** ¿Qué clase del DOM permite dar de baja de golpe todos los listeners registrados con su `signal`?

### Una idea, seis sintaxis (y el caso base en C) · Teoría

Para despachar eventos solo hace falta poder guardar **«qué función llamar» en una variable**. Todos los lenguajes lo permiten:

| Lenguaje | Mecanismo | Guardar una referencia a la función | ¿Con contexto? |
| --- | --- | --- | --- |
| C | Puntero a función | `void (*cb)(int) = al_pulsar;` | No |
| C++ | `std::function`, lambdas | `std::function<void(int)> cb = [&](int id) { … };` | Sí (captura) |
| C# | Delegados y eventos | `boton.Click += AlPulsar;` | Sí (objeto) |
| Java | Interfaz funcional, `this::m` | `boton.addActionListener(this::alPulsar);` | Sí (objeto) |
| Kotlin | Tipos función, lambdas, `::f` | `val cb: (Int) -> Unit = ::alPulsar` | Sí (captura) |
| Python / JS | Funciones de primera clase | `btn.addEventListener("click", alPulsar)` | Sí (clausura) |

Fíjate en la última columna: **C es el único que entrega solo la dirección del código**. Lo resolveremos con `void *ctx`.

### Punteros a función en C

Si se entiende aquí, se entiende en cualquier lenguaje: es lo que hay debajo de todos los demás.

```c
typedef enum { EV_BOTON, EV_TIMER, EV_UART, EV_COUNT } ev_tipo_t;

typedef struct {
    ev_tipo_t tipo;
    int       dato;
} evento_t;

/* El «tipo función» de C */
typedef void (*handler_t)(const evento_t *ev, void *ctx);

static void al_pulsar(const evento_t *ev, void *ctx) {
    printf("Botón %d\n", ev->dato);
}

handler_t h = al_pulsar;   /* guardar */
h(&ev, NULL);              /* invocar */
```

1. **El evento es un dato**: un tipo (`enum`) más su carga útil. Igual que la `sealed class EventoAgenda` de la U1.
2. **El contrato es un `typedef`**: equivale a `(Evento, Ctx) -> Unit` en Kotlin; fija parámetros y retorno de todo handler.
3. **El nombre es la dirección**: sin paréntesis, `al_pulsar` es un valor que se guarda, se pasa… y se mete en una tabla.

> [!TRUCO] ¿Para qué sirve EV_COUNT?
> Como los `enum` de C empiezan en 0, el último valor vale exactamente «cuántos tipos hay». Sirve para dimensionar tablas y validar índices.

**Preguntas**

**11.** ¿Qué lenguaje de la tabla guarda **solo la dirección del código**, sin contexto?

   a) Kotlin
   b) C#
   c) JavaScript
   d) C

**12.** ¿Qué palabra clave de C se usa para dar nombre al tipo puntero a función `handler_t`?

**13.** ¿Qué hace `handler_t h = al_pulsar;`?

   a) Llama a `al_pulsar` y guarda lo que devuelve
   b) Guarda la dirección de `al_pulsar` en `h`, sin llamarla
   c) Copia el código de la función en `h`
   d) No compila: falta `&`

### Del switch a la dispatch table · Teoría

La asociación *tipo de evento → handler* deja de ser **código** y pasa a ser un **dato**.

**Antes · el código decide**

```c
void despachar(const evento_t *ev) {
    switch (ev->tipo) {
    case EV_BOTON: al_pulsar(ev, NULL);  break;
    case EV_TIMER: al_vencer(ev, NULL);  break;
    case EV_UART:  al_recibir(ev, NULL); break;
    default: break;
    }
}
```

**Después · los datos deciden**

```c
static const handler_t tabla[EV_COUNT] = {
    [EV_BOTON] = al_pulsar,
    [EV_TIMER] = al_vencer,
    [EV_UART]  = al_recibir,
};

void despachar(const evento_t *ev) {
    if (ev->tipo < EV_COUNT && tabla[ev->tipo])
        tabla[ev->tipo](ev, NULL);
}
```

![Llega un evento con tipo = EV_TIMER. Su tipo se usa como índice en tabla[EV_COUNT], que tiene tres filas: EV_BOTON con al_pulsar, EV_TIMER con al_vencer (resaltada) y EV_UART con al_recibir. De la fila elegida se llama al handler al_vencer(ev, NULL). Debajo, la validación previa: índice en rango y entrada no nula.](/img/pde/tema-2/2a-dispatch-table.svg "Dispatch table: el tipo de evento es el índice; la fila elegida dice a quién llamar")

<div class="cards">
  <div class="card"><h4>Coste constante</h4><p>Un acceso por índice, haya 3 eventos o 300.</p></div>
  <div class="card"><h4>Crecer = añadir una fila</h4><p><code>despachar()</code> no se vuelve a tocar.</p></div>
  <div class="card"><h4>Modificable en ejecución</h4><p>Sin <code>const</code>, la tabla admite <code>registrar()</code> y dar de baja.</p></div>
  <div class="card"><h4>Siempre validar</h4><p>Índice en rango y entrada no nula antes de llamar.</p></div>
</div>

> [!NOTA] Inicializadores designados
> `[EV_BOTON] = al_pulsar` es sintaxis de C99: rellena la posición indicada. Las posiciones que no nombres quedan a `NULL` (en una tabla `static`), de ahí la comprobación `tabla[ev->tipo]`.

> [!EXAMEN]
> «Convierte este `switch` en una tabla de despacho y explica qué ganas» es un ejercicio típico. Las cuatro tarjetas de arriba son la respuesta.

**Preguntas**

**14.** ¿Qué ventajas tiene la dispatch table frente al `switch`?

   a) Coste constante: un acceso por índice
   b) Elimina la necesidad de validar el índice
   c) Añadir un evento es añadir una fila; `despachar()` no se toca
   d) El compilador comprueba que todos los tipos tienen handler
   e) Sin `const`, se puede registrar y dar de baja en ejecución

**15.** ¿Para qué sirve `ev->tipo < EV_COUNT && tabla[ev->tipo]`?

   a) Para ordenar los eventos por prioridad
   b) Para no salirse de la tabla y no llamar a un puntero nulo
   c) Para que el despacho sea síncrono
   d) Para liberar el evento después de procesarlo

### Traza la tabla en C · Ejercicio

Con esta tabla (fíjate en qué filas están rellenas):

```c
static void al_pulsar(const evento_t *ev, void *ctx) { printf("Botón %d\n", ev->dato); }
static void al_vencer(const evento_t *ev, void *ctx) { printf("Timer\n"); }

static const handler_t tabla[EV_COUNT] = {
    [EV_BOTON] = al_pulsar,
    [EV_TIMER] = al_vencer,
};

void despachar(const evento_t *ev) {
    if (ev->tipo < EV_COUNT && tabla[ev->tipo])
        tabla[ev->tipo](ev, NULL);
}
```

…se despachan, en este orden, estos eventos:

| # | tipo | dato |
| --- | --- | --- |
| 1 | `EV_BOTON` | 1 |
| 2 | `EV_UART` | 65 |
| 3 | `EV_TIMER` | 0 |
| 4 | `7` (valor corrupto) | 9 |
| 5 | `EV_BOTON` | 2 |

Recorre la tabla a mano: para cada evento, ¿pasa la validación? ¿Qué handler se llama?

> [!TRUCO] Valores del enum
> `EV_BOTON = 0`, `EV_TIMER = 1`, `EV_UART = 2`, `EV_COUNT = 3`.

<details><summary>Ver traza</summary>
<p>1 → <code>al_pulsar</code>: «Botón 1». 2 → índice válido pero <code>tabla[EV_UART]</code> es <code>NULL</code>: no hace nada. 3 → «Timer». 4 → 7 no es menor que 3: se descarta. 5 → «Botón 2».</p>
</details>

**Preguntas**

**16.** ¿Cuántas líneas se imprimen en total?

**17.** Si quitas la comprobación `ev->tipo < EV_COUNT`, ¿qué pasa con el evento 4?

   a) Nada: C ignora los índices fuera de rango
   b) Se llama a `al_pulsar` por defecto
   c) Se lee fuera de la tabla y se salta a una dirección cualquiera: comportamiento indefinido
   d) El compilador lo detecta y no compila

**18.** Quieres atender un nuevo evento `EV_ADC`. Con la tabla, ¿qué tocas?

   a) Añades el valor al `enum` (antes de `EV_COUNT`) y una fila `[EV_ADC] = al_medir`; `despachar()` no cambia
   b) Añades un `case EV_ADC` dentro de `despachar()`
   c) Reescribes `despachar()` con un bucle
   d) Nada: la tabla detecta el evento sola

### La misma tabla en Kotlin y JS, con varios suscriptores · Teoría

Un mapa *tipo de evento → lista de handlers*: aquí se encuentran listener y handler.

```kotlin
enum class Tipo { ANADIR, LISTAR, ELIMINAR, SALIR }
data class Evento(val tipo: Tipo, val dato: String = "")
typealias Handler = (Evento) -> Unit

class Despachador {
    private val tabla = mutableMapOf<Tipo, MutableList<Handler>>()

    fun on(tipo: Tipo, h: Handler) {
        tabla.getOrPut(tipo) { mutableListOf() } += h
    }
    fun despachar(ev: Evento) {
        tabla[ev.tipo]?.toList()?.forEach { it(ev) }
    }
}

d.on(Tipo.ANADIR) { tareas += it.dato }
d.on(Tipo.ANADIR, ::registrarEnLog)        // 2º suscriptor
d.despachar(Evento(Tipo.ANADIR, "Estudiar U2"))
```

1. **La tabla**: clave = tipo de evento; valor = lista de handlers. Es la lista de callbacks de la Clase 1, una por tipo.
2. **Registrar = escribir en la tabla**: `on()` crea la fila si no existe. Lambdas y referencias `::f` conviven.
3. **Despachar = buscar y difundir**: sin `when`. El bucle de eventos **no sabe qué significa** ningún evento: solo entrega.

```js
class Despachador {
  #tabla = new Map();                       // tipo -> [handlers]
  on(tipo, h) {
    if (!this.#tabla.has(tipo)) this.#tabla.set(tipo, []);
    this.#tabla.get(tipo).push(h);
  }
  despachar(ev) {
    const lista = this.#tabla.get(ev.tipo) ?? [];
    [...lista].forEach((h) => h(ev));
  }
}
d.on("ANADIR", (ev) => tareas.push(ev.dato));
d.on("ANADIR", registrarEnLog);
d.despachar({ tipo: "ANADIR", dato: "Estudiar U2" });
```

Es, en pequeño, lo que hacen por dentro **`EventEmitter`** en Node y **`EventTarget`** en el navegador.

> [!CUIDADO] Sin enum, sin red
> En JS la clave es un texto: si escribes `"ANADRI"`, nadie avisa. En Kotlin el `enum` hace que el compilador te pare.

**Preguntas**

**19.** En `despachar()`, ¿para qué sirven `toList()` (Kotlin) y `[...lista]` (JS)?

   a) Para recorrer una copia: un handler puede dar de alta o de baja sin romper la ronda
   b) Para ordenar los handlers
   c) Para convertir el evento en texto
   d) Para ejecutar los handlers en paralelo

**20.** En la versión JS registras `d.on("ANADRI", h)` por error. ¿Qué ocurre al despachar `{ tipo: "ANADIR" }`?

   a) Error de sintaxis al cargar el script
   b) Excepción en `despachar()`
   c) Se llama a `h` igualmente
   d) `h` no se llama nunca y nadie avisa

### Predice la salida del despachador · Ejercicio

Usa el `Despachador` de Kotlin del paso anterior:

```kotlin
val tareas = mutableListOf<String>()

fun registrarEnLog(ev: Evento) = println("log: ${ev.tipo} ${ev.dato}")

fun main() {
    val d = Despachador()
    d.on(Tipo.ANADIR) { tareas += it.dato }
    d.on(Tipo.ANADIR, ::registrarEnLog)
    d.on(Tipo.LISTAR) { println("tareas: $tareas") }

    d.despachar(Evento(Tipo.ANADIR, "Estudiar U2"))
    d.despachar(Evento(Tipo.ELIMINAR, "x"))
    d.despachar(Evento(Tipo.ANADIR, "Repasar C"))
    d.despachar(Evento(Tipo.LISTAR))
}
```

Pistas para la traza:

- Para `ANADIR` hay **dos** handlers en la fila: el primero no imprime nada, el segundo sí.
- ¿Hay fila para `ELIMINAR`? Mira qué hace `tabla[ev.tipo]?` cuando la clave no existe.
- `println` de una `MutableList` imprime sus elementos entre corchetes, separados por coma y espacio.

> [!NOTA] Interpolación
> `"log: ${ev.tipo}"` imprime el **nombre** de la constante del `enum` (`ANADIR`), porque `toString()` de un `enum` devuelve su nombre.

**Preguntas**

**21.** Ordena las líneas que imprime el programa.

   Ordena: log: ANADIR Estudiar U2 · log: ANADIR Repasar C · tareas: [Estudiar U2, Repasar C]

**22.** ¿Qué ocurre con `Evento(Tipo.ELIMINAR, "x")`?

   a) Lanza `NullPointerException`
   b) Nada: no hay fila, `tabla[ev.tipo]` es `null` y el `?.` corta la cadena
   c) Se ejecutan los handlers de `ANADIR`
   d) Elimina la última tarea

### Callback table frente a dispatch table · Teoría

Las dos son **tablas de referencias a función**. Cambia **quién elige la entrada y cuándo**.

**Callback table**

```c
typedef struct {
    void (*al_conectar)(void *ctx);
    void (*al_recibir)(const uint8_t *buf, size_t n, void *ctx);
    void (*al_cerrar)(void *ctx);
} red_callbacks_t;

red_iniciar(&mis_callbacks, &mi_ctx);
```

**Dispatch table**

```c
static const handler_t tabla[EV_COUNT] = {
    [EV_BOTON] = al_pulsar,
    [EV_TIMER] = al_vencer,
    [EV_UART]  = al_recibir,
};
tabla[ev->tipo](ev, ctx);
```

| | Callback table | Dispatch table |
| --- | --- | --- |
| Entradas | Con nombre, fijadas al compilar. | Elegidas por un dato en ejecución: tipo de evento, código de operación, comando recibido. |
| Uso | El cliente rellena los huecos que le interesan y entrega el struct entero. | El despachador indexa la tabla con el dato que llega. |
| Es… | Una **interfaz**: vtables de C++, drivers, pilas de red o Bluetooth. | Un **selector**: bucles de eventos, intérpretes, tablas de interrupciones, protocolos. |

> [!ANALOGIA]
> La **callback table** es el formulario de contacto que rellenas para una empresa: «si hay incidencia, llamad aquí; si se envía el paquete, escribid aquí». La **dispatch table** es la centralita: según la extensión marcada (un dato), conecta con una línea u otra.

Los obligatorios de la unidad lo resumen así: la callback table guarda funciones o delegados **junto a su contexto**; la dispatch table asocia **cada tipo de evento con uno o varios handlers**. La sintaxis cambia entre lenguajes; la asociación evento → reacción permanece.

**Preguntas**

**23.** Una librería Bluetooth te pide un `struct` con `al_conectar`, `al_recibir` y `al_desconectar`. Es una…

   a) Dispatch table
   b) Callback table
   c) Cola de eventos
   d) Suscripción cancelable

**24.** Un intérprete lee un byte de código de operación y llama a la función correspondiente en un array. Es una…

   a) Callback table
   b) Clausura
   c) Dispatch table
   d) Referencia débil

### void *ctx: el contexto que acompaña al callback · Teoría

En C no hay clausuras. ¿Cómo sabe un handler **sobre qué LED** actuar? Quien registra entrega **función + puntero opaco**; quien despacha devuelve ese puntero en cada llamada.

```c
typedef struct { handler_t fn; void *ctx; } entrada_t;
static entrada_t tabla[EV_COUNT];

void registrar(ev_tipo_t t, handler_t fn, void *ctx) {
    tabla[t] = (entrada_t){ fn, ctx };
}

typedef struct { int pin; bool encendido; } led_t;

static void conmutar(const evento_t *ev, void *ctx) {
    led_t *led = ctx;                 /* recuperar contexto */
    led->encendido = !led->encendido;
    gpio_set_level(led->pin, led->encendido);
}

static led_t rojo = { .pin = 2 }, verde = { .pin = 3 };
registrar(EV_BOTON, conmutar, &rojo);
registrar(EV_TIMER, conmutar, &verde);
```

1. **La entrada guarda la pareja** función + contexto. El despachador no sabe qué hay tras el `void*`: solo lo devuelve.
2. **El handler lo recupera** convirtiendo el `void*` a su tipo real. Ni variables globales ni dependencia de una GUI.
3. **Un handler, N contextos**: el mismo código sirve al LED rojo y al verde.

### Una clausura es función + contexto

| Kotlin · lo hace el compilador | C · lo haces tú |
| --- | --- |
| `val led = Led(pin = 2)`<br>`boton.onClick { led.conmutar() }` | `static led_t led = { .pin = 2 };`<br>`registrar(EV_BOTON, conmutar, &led);` |

APIs reales donde viaja el contexto: ESP-IDF `esp_event_handler_register(…, fn, arg)`, FreeRTOS `xTaskCreate(fn, …, pvParameters, …)`, POSIX `pthread_create(&hilo, attr, fn, arg)`.

> [!IMPORTANTE] Ownership sin recolector de basura
> El contexto **debe vivir más que el registro**: nunca una variable local de una función que retorna. Con memoria dinámica: **primero dar de baja, después `free()`**. Misma regla que en Android: toda alta tiene su baja.

**Preguntas**

**25.** ¿Qué sabe el despachador sobre lo que hay detrás de `void *ctx`?

   a) Que siempre es un `led_t`
   b) Su tamaño, para copiarlo
   c) Nada: solo lo guarda y lo devuelve en cada llamada
   d) Que es una variable global

**26.** ¿Qué falla en este código? `void configurar(void) { led_t azul = { .pin = 4 }; registrar(EV_UART, conmutar, &azul); }`

   a) Nada, es correcto
   b) `azul` es local: deja de existir al retornar `configurar()` y la tabla se queda con un puntero colgante
   c) Falta un `free(azul)`
   d) `conmutar` no acepta LEDs azules

**27.** Con un contexto reservado con `malloc`, ordena el final de su vida.

   Ordena: Dar de baja el handler en la tabla · Liberar el contexto con free()

### El despacho completo, sin ninguna GUI · Teoría

Las mismas cuatro piezas en una app Android, en un servidor o en un microcontrolador:

<div class="flow">
  <div><b>Fuentes</b><small>botón (ISR), temporizador, UART / red</small></div>
  <div><b>Cola de eventos</b><small>{tipo, dato} · desacopla</small></div>
  <div><b>Bucle de despacho</b><small>ev = sacar() · no sabe qué significa</small></div>
  <div><b>Tabla tipo → (handler, ctx)</b><small>asocia y entrega contexto</small></div>
</div>

Ejemplo de tabla en ejecución:

| Tipo | Handler | Contexto |
| --- | --- | --- |
| `EV_BOTON` | `conmutar` | `&rojo` |
| `EV_TIMER` | `conmutar` | `&verde` |
| `EV_UART` | `al_recibir` | `&buf` |

Es **portable** porque ninguna pieza menciona botones de pantalla ni vistas: solo tipos de evento, referencias a función y un puntero de contexto.

### Diseño de un dispatcher

- `registrar(tipo, callback, contexto)` devuelve una **suscripción identificable**.
- `despachar(evento)` localiza los handlers por tipo y aplica la **política de orden**.
- `desregistrar(suscripción)` retira el interés y **define qué pasa con las entregas pendientes**.

La tabla de despacho **separa la fuente del código de cada reacción**. En C++ moderno, la misma idea cabe en tres líneas:

```cpp
using Handler = std::function<void(int)>;
std::unordered_map<int, Handler> t;
t[1] = [](int x) { log(x); };
t.at(tipo)(dato);
```

Aquí el contexto puede viajar en el evento o en la clausura (la lambda captura lo que necesite).

**Preguntas**

**28.** Ordena los pasos de un despacho sin GUI.

   Ordena: Buscar: el dispatcher consulta la tabla del tipo de evento · Continuar: aplica la política de orden, fallos y bajas · Crear el evento: la fuente comunica tipo, origen y datos · Invocar: entrega el evento y, si procede, un contexto · Seleccionar: recupera uno o varios callbacks registrados

**29.** ¿Por qué este esquema sirve igual en Android, un servidor o un ESP32?

   a) Porque todos usan Kotlin
   b) Porque ninguna pieza depende de la GUI: solo tipos de evento, referencias a función y contexto
   c) Porque todos tienen recolector de basura
   d) Porque el bucle de despacho conoce el significado de cada evento

### Repaso: lista de comprobación de la Unidad 2 · Repaso

Seis preguntas que hacerse **antes de dar por bueno** un diseño dirigido por eventos:

| # | Pregunta | Qué mirar |
| --- | --- | --- |
| 1 | ¿Cuál es el contrato? | Firma del callback: parámetros, retorno y quién lo invoca. |
| 2 | ¿Síncrono o diferido? | En qué hilo y con qué pila corre; dónde se tratan sus excepciones. |
| 3 | ¿Cuántos suscriptores? | Uno o N por fuente; ¿algo depende del orden de invocación? |
| 4 | ¿Quién da de baja? | Dueño del callback, alta y baja simétricas, misma referencia o suscripción. |
| 5 | ¿Cómo se asocia evento → handler? | `when`/`switch`, o una tabla que crece añadiendo filas. |
| 6 | ¿De dónde saca el handler sus datos? | Clausura, objeto ligado o `void *ctx`; y cuánto vive ese contexto. |

> [!RESUMEN] Chuleta
> - Quien guarda un callback mantiene vivo **todo lo que captura** → fugas y callbacks fantasma.
> - **Toda alta tiene su baja**, en métodos simétricos y con la misma referencia.
> - Cuatro estrategias de ownership: baja explícita, suscripción cancelable, referencia débil, ligado al ciclo de vida.
> - Puntero a función (C) ≈ `std::function` ≈ delegado ≈ `::f` ≈ función de primera clase.
> - **Dispatch table**: la asociación tipo → handler como dato. Validar índice y no nulo.
> - **Callback table** = interfaz con huecos con nombre; **dispatch table** = selector por dato.
> - `void *ctx` = el contexto que en Kotlin captura la clausura. Debe vivir más que el registro.

> [!EXAMEN] Autoevaluación
> ¿Qué diferencia hay entre invocación síncrona y diferida? ¿Quién conserva la referencia y quién realiza la baja? ¿Qué ocurre si un handler falla o cambia el registro durante el despacho?

**Preguntas**

**30.** ¿Cuáles de estas son estrategias de ownership vistas en clase?

   a) Baja explícita en el método simétrico
   b) Registrar el listener dos veces por seguridad
   c) Suscripción cancelable devuelta por el alta
   d) Referencia débil en la fuente
   e) Alta ligada al ciclo de vida del suscriptor

**31.** Completa: «una clausura es función + …»

**32.** ¿Qué pareja de métodos del ciclo de vida está **mal** emparejada?

   a) onCreate ↔ onDestroy
   b) onStart ↔ onStop
   c) onResume ↔ onPause
   d) onStart ↔ onDestroy

### Solucionario

1. b) Porque el GPS guarda la lambda y la lambda captura `this` (la pantalla): hay una cadena de referencias fuertes desde algo que vive toda la app  
   Gps → listeners → lambda → PantallaMapa. Mientras el GPS viva, toda la cadena es alcanzable y no se puede liberar.
2. a) Fuga de memoria: pantallas cerradas que siguen ocupando memoria; c) Callbacks fantasma: se invoca código de una pantalla que ya no existe  
   El compilador no avisa de nada: el fallo aparece en ejecución, como **fuga** y como **callbacks fantasma**.
3. `onPause`  
   `onResume ↔ onPause`. Cada alta en su pareja simétrica.
4. b) Porque así fija `this` y es la misma referencia en el alta y en la baja  
   Una flecha escrita dentro de `addListener` sería un objeto nuevo imposible de pasar luego a `removeListener`. El campo da **una sola referencia**.
5. a) En la función de limpieza que devuelve el efecto  
   La función de limpieza del efecto es el «desmontar» simétrico del alta.
6. b) P2; c) P3  
   P2 cruza `onStart`/`onPause`; P3 da de baja una lambda distinta de la registrada.
7. c) Diez lambdas, cada una reteniendo su pantalla: fuga y callbacks fantasma  
   Las bajas nunca encuentran su lambda. Quedan diez lambdas y, con ellas, diez pantallas en memoria recibiendo posiciones.
8. b) La limpieza llega tarde y sin avisar: el listener puede seguir recibiendo eventos hasta que actúe el recolector  
   La referencia débil no retiene, pero **no sabes cuándo** desaparecerá: mientras tanto puede haber callbacks fantasma.
9. c) Quien guarda el objeto suscripción  
   Quien guarda la suscripción (o la función `cancelar`, o el `AbortController`) es el dueño; cancelarla es la baja.
10. `AbortController`  
   `AbortController`: al llamar a `abort()`, todos los listeners registrados con su `signal` se dan de baja.
11. d) C  
   Un puntero a función de C es solo una dirección. El contexto hay que pasarlo aparte (`void *ctx`).
12. `typedef`  
   `typedef void (*handler_t)(const evento_t *ev, void *ctx);` define el contrato de todos los handlers.
13. b) Guarda la dirección de `al_pulsar` en `h`, sin llamarla  
   Sin paréntesis, el nombre de la función es su **dirección**. La llamada llega después con `h(&ev, NULL)`.
14. a) Coste constante: un acceso por índice; c) Añadir un evento es añadir una fila; `despachar()` no se toca; e) Sin `const`, se puede registrar y dar de baja en ejecución  
   Validar sigue siendo obligatorio (índice en rango y entrada no nula), y el compilador no garantiza que todas las filas estén rellenas.
15. b) Para no salirse de la tabla y no llamar a un puntero nulo  
   Un índice fuera de rango leería memoria ajena; una entrada `NULL` haría saltar a la dirección 0. Ambas cosas cuelgan el programa (o algo peor).
16. `3`  
   Tres: «Botón 1», «Timer» y «Botón 2».
17. c) Se lee fuera de la tabla y se salta a una dirección cualquiera: comportamiento indefinido  
   C no comprueba los límites de un array. `tabla[7]` lee memoria ajena y se intenta llamar a lo que haya allí.
18. a) Añades el valor al `enum` (antes de `EV_COUNT`) y una fila `[EV_ADC] = al_medir`; `despachar()` no cambia  
   Crecer = añadir una fila. Esa es la gracia de pasar del código a los datos.
19. a) Para recorrer una copia: un handler puede dar de alta o de baja sin romper la ronda  
   La misma idea que en `Sensor.publicar`: difundir sobre una **foto** estable de la lista.
20. d) `h` no se llama nunca y nadie avisa  
   Son dos filas distintas de la tabla. El fallo es silencioso: por eso conviene usar constantes o tipos.
21. 1. log: ANADIR Estudiar U2 → 2. log: ANADIR Repasar C → 3. tareas: [Estudiar U2, Repasar C]  
   Cada `ANADIR` añade la tarea y la registra en el log; `ELIMINAR` no tiene fila; `LISTAR` imprime la lista acumulada.
22. b) Nada: no hay fila, `tabla[ev.tipo]` es `null` y el `?.` corta la cadena  
   Sin fila no hay nada que difundir. Igual que en el bus de la Clase 3: si nadie escucha, el evento se pierde en silencio.
23. b) Callback table  
   Entradas con nombre, fijadas al compilar, que tú rellenas: una **callback table** (una interfaz).
24. c) Dispatch table  
   La entrada la elige **un dato en ejecución** (el código de operación): **dispatch table**.
25. c) Nada: solo lo guarda y lo devuelve en cada llamada  
   Es un puntero **opaco**: el despachador no lo interpreta. Solo el handler sabe convertirlo a su tipo real.
26. b) `azul` es local: deja de existir al retornar `configurar()` y la tabla se queda con un puntero colgante  
   El contexto debe vivir más que el registro. Usa `static`, una global o memoria dinámica que liberes **después** de la baja.
27. 1. Dar de baja el handler en la tabla → 2. Liberar el contexto con free()  
   Si liberas primero, entre medias un despacho podría usar memoria ya liberada. Primero baja, después `free()`.
28. 1. Crear el evento: la fuente comunica tipo, origen y datos → 2. Buscar: el dispatcher consulta la tabla del tipo de evento → 3. Seleccionar: recupera uno o varios callbacks registrados → 4. Invocar: entrega el evento y, si procede, un contexto → 5. Continuar: aplica la política de orden, fallos y bajas  
   Crear → buscar → seleccionar → invocar → continuar. Ninguno de los pasos necesita una pantalla.
29. b) Porque ninguna pieza depende de la GUI: solo tipos de evento, referencias a función y contexto  
   El bucle **no sabe qué significa** cada evento; la tabla asocia tipo → reacción. Eso lo hace independiente de la plataforma.
30. a) Baja explícita en el método simétrico; c) Suscripción cancelable devuelta por el alta; d) Referencia débil en la fuente; e) Alta ligada al ciclo de vida del suscriptor  
   Las cuatro estrategias: baja explícita, suscripción cancelable, referencia débil y ligado al ciclo de vida.
31. `contexto`  
   Función + **contexto**: lo que en C pasas a mano con `void *ctx`, Kotlin lo hace solo al capturar variables.
32. d) onStart ↔ onDestroy  
   `onStart` se empareja con `onStop`. Cruzar pares deja la suscripción viva (o muerta) cuando no toca.

## Observer, pub/sub y MVVM

_Ponemos nombre a lo que ya habíamos construido: el patrón Observer del GoF con sus invariantes, el bus de publicación-suscripción con topics y la pantalla como un observador más en MVC y MVVM._

### Ya lo habéis escrito: el Observer del GoF · Teoría

Las dos clases anteriores construyeron las piezas; hoy les ponemos **nombre y contrato**.

| Lo que escribiste | Cómo se llama hoy |
| --- | --- |
| Clase 1 · `Sensor`: fuente con lista privada de listeners, alta, baja y difusión sobre una copia. | **Sujeto** (*Subject*) y **observadores**. Le faltaba estado propio y contrato. |
| Clase 2 · `Despachador`: `Map` tipo → lista de handlers; `on()` registra; `despachar()` difunde. | **Bus de eventos**; el tipo era el **topic**. |
| Lo que falta | La pantalla como un suscriptor cualquiera: **MVC / MVVM**. |

> [!NOTA] ¿Qué es el GoF?
> *Gang of Four*, la «Banda de los Cuatro»: Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides, autores de *Design Patterns: Elements of Reusable Object-Oriented Software* (1994), el catálogo clásico de patrones de diseño. Observer es uno de ellos.

### Dos roles, cuatro operaciones

<div class="cards">
  <div class="card"><h4>Sujeto · Subject / Observable</h4><p>Tiene <b>estado</b> (p. ej. <code>temperatura</code>) y una <b>lista</b> de observadores. Operaciones: <code>attach</code> (suscribir), <code>detach</code> (desuscribir) y <code>notify</code> (notificar), que recorre la lista y llama a <code>update()</code> en cada uno.</p></div>
  <div class="card"><h4>Observador · Observer</h4><p>Solo promete un método, <code>update()</code> (aquí <code>actualizar(t)</code>), que <b>él nunca llama</b>: se lo llama el sujeto. Pantalla pinta, Alarma avisa si se supera un umbral, Registro guarda.</p></div>
</div>

### Tres invariantes

1. **Solo el sujeto cambia su estado.** `private set`: nadie de fuera escribe `temperatura`.
2. **Sin cambio, no hay aviso.** `if (nueva == temperatura) return`.
3. **El observador no pregunta.** Le llaman. No hace *polling* ni conoce a los demás.

**Desacoplamiento**: el termómetro solo sabe que le escuchan «observadores». Añadir un cuarto mañana no toca ni una línea de `Termometro`.

> [!EXAMEN]
> Roles, las cuatro operaciones (attach / detach / notify / update) y las tres invariantes: tenlas a mano para definir Observer en dos líneas.

**Preguntas**

**1.** ¿Qué significa GoF?

   a) Generic Observer Framework
   b) Gang of Four: los cuatro autores del libro *Design Patterns* (1994)
   c) Graph of Functions
   d) Google Open Framework

**2.** ¿Cuáles son invariantes del Observer tal como lo hemos definido?

   a) Solo el sujeto cambia su estado
   b) Cada observador consulta periódicamente al sujeto
   c) Sin cambio, no hay aviso
   d) El observador no pregunta: le llaman
   e) Los observadores se conocen entre sí para coordinarse

### El sujeto en Kotlin: Termometro · Teoría

En clase se escribe en vivo en `pde-consola` (`Observer.kt`, paso 1), nombrando cada pieza al teclearla:

```kotlin
interface Observador {                        // update(): lo llama el sujeto
    fun actualizar(temperatura: Double)
}

class Termometro {                            // Subject / Observable
    private val observadores = mutableListOf<Observador>()

    var temperatura = 20.0
        private set                           // solo el sujeto lo cambia

    fun suscribir(o: Observador)   { observadores += o }   // attach
    fun desuscribir(o: Observador) { observadores -= o }   // detach

    fun medir(nueva: Double) {
        if (nueva == temperatura) return      // sin cambio -> sin aviso
        temperatura = nueva
        notificar()
    }

    private fun notificar() {
        for (o in observadores.toList()) o.actualizar(temperatura)
    }
}
```

<div class="cards">
  <div class="card"><h4>1 · El contrato en una interfaz</h4><p><code>Observador</code> solo promete <code>actualizar()</code>. El sujeto no sabe nada más de quien le escucha.</p></div>
  <div class="card"><h4>2 · Estado con private set</h4><p>Se lee desde fuera, se escribe solo desde <code>medir()</code>. Es lo que lo distingue del <code>Sensor</code> de la Clase 1, que no tenía estado.</p></div>
  <div class="card"><h4>3 · Notificar solo si cambia</h4><p>Y sobre una copia de la lista, como en <code>Sensor.publicar</code>. <code>notificar()</code> es privado: nadie fuerza avisos desde fuera.</p></div>
</div>

> [!TRUCO] Lee cada línea con su nombre GoF
> `suscribir` = attach, `desuscribir` = detach, `notificar` = notify, `actualizar` = update. Si en un examen te dan otro código, busca esas cuatro piezas.

**Preguntas**

**3.** ¿Por qué `notificar()` es `private`?

   a) Porque Kotlin no permite funciones públicas que recorran listas
   b) Para que nadie de fuera fuerce avisos: solo el sujeto decide notificar cuando su estado cambia
   c) Para que los observadores no puedan darse de baja
   d) Para que sea más rápido

**4.** ¿Qué dos palabras de Kotlin hacen que `temperatura` se pueda leer desde fuera pero solo escribir desde dentro de la clase?

### Observadores que no se conocen · Ejercicio

Tres observadores que no saben nada unos de otros; el termómetro no conoce el tipo de ninguno:

```kotlin
class Pantalla : Observador {
    override fun actualizar(temperatura: Double) =
        println("Pantalla: %.1f ºC".format(temperatura))
}

class Alarma(private val umbral: Double) : Observador {
    override fun actualizar(temperatura: Double) {
        if (temperatura > umbral) println("ALARMA: $temperatura > $umbral")
    }
}

class Registro : Observador {
    val historial = mutableListOf<Double>()
    override fun actualizar(temperatura: Double) { historial += temperatura }
}

fun main() {
    val termometro = Termometro()
    termometro.suscribir(Pantalla())
    termometro.suscribir(Alarma(umbral = 30.0))
    termometro.suscribir(Registro())

    termometro.medir(22.5)
    termometro.medir(22.5)   // igual: no notifica
    termometro.medir(31.0)   // salta la alarma
}
```

(Detalle: `String.format` usa la configuración regional; en un equipo en español `%.1f` puede escribir `22,5`. La salida de clase usa punto.)

Predice la salida y después juega con el laboratorio: suscribe y da de baja observadores, mide temperaturas repetidas y por encima del umbral.

> _Aquí hay un simulador interactivo (`observer-lab`): ábrelo en la web._
> [!NOTA] Pregunta de clase
> ¿Qué pasaría en un **contador de pantalla** («veces que ha cambiado la temperatura») si el sujeto avisara sin cambio real? Contaría cambios que no existen. La invariante «sin cambio, no hay aviso» protege a los observadores que **cuentan** o **acumulan**.

**Preguntas**

**5.** Ordena las líneas que imprime `main()`.

   Ordena: ALARMA: 31.0 > 30.0 · Pantalla: 22.5 ºC · Pantalla: 31.0 ºC

**6.** ¿Por qué la segunda medida de 22.5 no imprime nada?

   a) Porque `Pantalla` ignora valores repetidos
   b) Porque `medir()` hace `return` si el valor no cambia: sin cambio no hay aviso
   c) Porque la lista de observadores se vacía tras la primera notificación
   d) Porque `Registro` consume el evento

**7.** Al acabar `main()`, ¿qué contiene el `historial` del `Registro`?

   a) [22.5, 22.5, 31.0]
   b) [31.0]
   c) [22.5, 31.0]
   d) [] (vacío)

### El mismo Observer en JavaScript · Teoría

Sin interfaces: el contrato es **«tener un método `actualizar()`»**.

```js
class Termometro {                         // sujeto
  #observadores = [];
  #temperatura = 20.0;                     // privado: solo lo cambia el sujeto

  suscribir(o)   { this.#observadores.push(o); }
  desuscribir(o) { this.#observadores = this.#observadores.filter(x => x !== o); }

  medir(nueva) {
    if (nueva === this.#temperatura) return;   // sin cambio -> sin aviso
    this.#temperatura = nueva;
    this.#notificar();
  }

  #notificar() {
    for (const o of [...this.#observadores]) o.actualizar(this.#temperatura);
  }
}

class Pantalla { actualizar(t) { console.log(`Pantalla: ${t.toFixed(1)} ºC`); } }

class Alarma {
  constructor(umbral) { this.umbral = umbral; }
  actualizar(t) { if (t > this.umbral) console.log(`ALARMA: ${t} > ${this.umbral}`); }
}
```

<div class="cards">
  <div class="card"><h4>1 · Contrato implícito</h4><p>No hay <code>interface</code>: cualquier objeto con <code>actualizar()</code> vale. El error, si lo hay, aparece <b>en ejecución</b>, no al compilar.</p></div>
  <div class="card"><h4>2 · La baja por identidad</h4><p><code>filter(x => x !== o)</code>: otra instancia «igual» no quita nada. Igual que <code>removeEventListener</code>.</p></div>
  <div class="card"><h4>3 · Observadores sin herencia</h4><p><code>Pantalla</code> y <code>Alarma</code> no comparten superclase. <i>Duck typing</i> puro: «si camina como un pato y grazna como un pato…».</p></div>
</div>

> [!NOTA] Campos con #
> `#observadores` y `#temperatura` son campos **privados** de JavaScript: fuera de la clase ni se leen ni se escriben. Cumplen la invariante «solo el sujeto cambia su estado».

**Preguntas**

**8.** Suscribes por error `{ actualisar(t) { … } }` (con «s»). ¿Cuándo te enteras?

   a) Al compilar, porque no implementa la interfaz
   b) Al suscribirlo, `suscribir()` lanza un error
   c) Al notificar: `o.actualizar` no es una función y salta un `TypeError` en ejecución
   d) Nunca, JavaScript lo corrige solo

**9.** `t.suscribir(new Pantalla()); t.desuscribir(new Pantalla());` ¿Cuántos observadores quedan?

   a) 0
   b) 1
   c) 2
   d) Depende del navegador

### Traduce Sensor.kt y elige push o pull · Ejercicio

Dos minutos en voz alta: el `Sensor` de la Clase 1, con vocabulario GoF.

| En `Sensor.kt` (Clase 1) | En el GoF |
| --- | --- |
| `addListener(l)` | attach / suscribir |
| `removeListener(l)` | detach / desuscribir |
| `publicar(dato)` | notify (pero sin estado propio) |
| `val enPantalla: Listener` | un observador cuyo update es la lambda |
| **¿Qué le falta?** | estado propio y «sin cambio, no hay aviso» |

### Un matiz del GoF: push frente a pull

<div class="cards">
  <div class="card"><h4>PUSH</h4><p>El sujeto <b>empuja el dato</b>: <code>actualizar(temperatura)</code>. Simple. Es lo que hemos escrito y lo que hace un listener de Android.</p></div>
  <div class="card"><h4>PULL</h4><p>El sujeto <b>solo avisa</b>: <code>actualizar(sujeto)</code>. El observador pregunta <code>sujeto.temperatura</code>. Cada uno coge lo que necesita cuando el estado es grande.</p></div>
</div>

> [!TRUCO] Regla práctica
> **Push** cuando el dato es pequeño y todos lo quieren; **pull** cuando el estado es grande y cada observador usa una parte distinta.

Decide para cada caso:

- **Caso 1.** Un sensor de temperatura que avisa a una pantalla, un log y una alarma. Todos quieren el mismo `Double`.
- **Caso 2.** Un documento de un editor con texto, estilos, cursor, selección e historial. La barra de estado quiere el número de palabras; la miniatura, el texto; el botón «deshacer», el historial.

**Preguntas**

**10.** Caso 1 (dato pequeño, todos lo quieren):

   a) Pull
   b) Push
   c) Ninguno: hay que usar polling
   d) Da igual, pull siempre es mejor

**11.** Caso 2 (estado grande, cada uno usa una parte distinta):

   a) Push: se envía el documento entero a todos en cada cambio
   b) Pull: se avisa con `actualizar(documento)` y cada observador lee la parte que necesita
   c) Ninguno: cada observador debe guardar su propia copia del documento
   d) Pub/sub con un topic por carácter

**12.** ¿Qué le falta a `Sensor` para ser un sujeto Observer completo?

   a) Estado propio
   b) Una lista de listeners
   c) La regla «sin cambio, no hay aviso»
   d) Operaciones de alta y baja

### Aparece un intermediario: el bus · Teoría

Quien publica **no tiene lista**; quien se suscribe **no le conoce**. Los une un **topic**.

```kotlin
typealias Suscriptor = (Any) -> Unit

object Bus {                                  // uno para toda la aplicación
    private val topics = mutableMapOf<String, MutableList<Suscriptor>>()

    fun suscribir(topic: String, s: Suscriptor): () -> Unit {
        topics.getOrPut(topic) { mutableListOf() } += s
        return { topics[topic]?.remove(s) }  // función de baja (Clase 2)
    }

    fun publicar(topic: String, dato: Any) {
        val lista = topics[topic] ?: return   // nadie escucha: se pierde sin error
        for (s in lista.toList()) s(dato)
    }
}
```

<div class="cards">
  <div class="card"><h4>1 · object: uno por aplicación</h4><p>Es el <code>Despachador</code> de la Clase 2 con el tipo sustituido por un <code>String</code>. En Node: <code>EventEmitter</code>; entre máquinas: Kafka, RabbitMQ, MQTT.</p></div>
  <div class="card"><h4>2 · suscribir devuelve la baja</h4><p>El patrón de la suscripción cancelable. Quien guarda la función es el dueño.</p></div>
  <div class="card"><h4>3 · ?: return</h4><p>Si nadie escucha, el mensaje se pierde en silencio. Decisión de diseño, no bug… y tiene consecuencias.</p></div>
</div>

<div class="flow">
  <div><b>Publicador</b><small>no conoce a nadie</small></div>
  <div><b>Bus</b><small>topic → lista de suscriptores</small></div>
  <div><b>Suscriptores</b><small>no conocen al publicador</small></div>
</div>

> [!ANALOGIA]
> Un tablón de anuncios por secciones: quien cuelga un anuncio en «Pisos» no sabe quién lo leerá, y quien mira «Pisos» no sabe quién lo colgó. Si nadie mira la sección «Bicicletas», ese anuncio no le llega a nadie… y nadie se entera.

> [!NOTA] Topics jerárquicos
> Nombres como `"sensor/temperatura"` o `"usuario/login"` imitan el estilo de MQTT, donde los topics se organizan como rutas.

**Preguntas**

**13.** `Bus.publicar("sensor/humedad", 55)` y nadie se ha suscrito a ese topic. ¿Qué pasa?

   a) Lanza una excepción
   b) Se guarda hasta que llegue un suscriptor
   c) El mensaje se pierde en silencio
   d) Se reenvía a todos los topics

**14.** ¿Qué devuelve `Bus.suscribir(...)`?

   a) El número de suscriptores del topic
   b) Una función que, al llamarla, da de baja a ese suscriptor
   c) El último dato publicado en el topic
   d) Nada (`Unit`)

### Predice la salida del bus · Ejercicio

El mismo bus en JavaScript (un `EventEmitter` escrito a mano):

```js
const Bus = {
  topics: new Map(),
  suscribir(topic, s) {                                   // on()
    if (!this.topics.has(topic)) this.topics.set(topic, []);
    this.topics.get(topic).push(s);
    return () => this.topics.set(topic,                   // off()
      this.topics.get(topic).filter(x => x !== s));
  },
  publicar(topic, dato) {                                 // emit()
    const lista = this.topics.get(topic);
    if (!lista) return;                                   // nadie escucha: silencio
    for (const s of [...lista]) s(dato);
  },
};

const bajaLog = Bus.suscribir("sensor/temperatura", d => console.log(`log: ${d}`));
Bus.suscribir("sensor/temperatura", d => { if (d > 30) console.log(`alarma: ${d}`); });
Bus.suscribir("usuario/login", u => console.log(`bienvenida a ${u}`));

Bus.publicar("sensor/temperatura", 28.0);
Bus.publicar("sensor/humedad", 55);          // nadie suscrito
Bus.publicar("usuario/login", "ana");
bajaLog();
Bus.publicar("sensor/temperatura", 35.0);
```

Traza cada `publicar`: ¿qué topic?, ¿quién está suscrito en ese momento?, ¿qué condición se cumple?

> [!NOTA] 28 y no 28.0
> En JavaScript todos los números son `Number`: `28.0` se imprime como `28`. La versión Kotlin del mismo programa imprimiría `log: 28.0` y `alarma: 35.0`.

Equivalencias: Node `EventEmitter` (`on` / `emit` / `off`), `CustomEvent` en el DOM y, entre procesos, MQTT o Kafka: los mismos tres verbos.

**Preguntas**

**15.** Ordena las líneas que imprime el programa.

   Ordena: alarma: 35 · bienvenida a ana · log: 28

**16.** ¿Por qué no aparece `log: 35`?

   a) Porque 35 supera el umbral y la alarma consume el evento
   b) Porque `bajaLog()` quitó ese suscriptor antes de publicar 35
   c) Porque el bus solo entrega el primer mensaje de cada topic
   d) Porque `sensor/temperatura` se borró al publicar humedad

### Observer frente a pub/sub · Teoría

Se gana **desacoplamiento total**; se pierde **la ayuda del compilador**.

![Dos esquemas lado a lado. Observer: el sujeto Termometro guarda la lista y llama a actualizar(t) en Pantalla, Alarma y Registro, que se suscribieron a él. Publicación-suscripción: el Publicador publica en el Bus, en el topic sensor/temperatura, y el bus entrega a los suscriptores log y alarma; nadie conoce a nadie, solo comparten el topic.](/img/pde/tema-2/2a-observer-vs-pubsub.svg "Observer: el sujeto tiene la lista. Pub/sub: la lista está en el bus y solo se comparte el topic")

| | Observer (GoF) | Publicación-suscripción |
| --- | --- | --- |
| Quién conoce a quién | El observador tiene la referencia del sujeto (se suscribe a él). | Nadie conoce a nadie; comparten un topic. |
| Quién guarda la lista | El sujeto. | El intermediario (bus, broker). |
| Quién puede notificar | Solo el sujeto, al cambiar su estado. | Cualquiera que publique en el topic. |
| Tipo del dato | Fuerte: la interfaz lo fija (`Double`). | Débil: `Any` y un `String` como clave → errores en ejecución. |
| Si nadie escucha | El sujeto lo sabe (lista vacía). | El mensaje se pierde y nadie se entera. |
| Cuándo usarlo | Pocos sujetos, relación clara, misma unidad de código. | Muchos productores/consumidores; módulos, procesos o máquinas distintos. |

### Riesgos heredados de la Clase 2, agravados

- **Reentrancia**: un suscriptor que publica en su propio topic dispara una difusión dentro de otra.
- **Suscriptores lentos**: la difusión es síncrona; uno lento retrasa a todos los que van detrás.
- **Trazabilidad**: «quién escucha qué» ya no se ve siguiendo una referencia; hay que **buscar el string** por todo el proyecto.

> [!CUIDADO] Un topic mal escrito no da error
> `Bus.suscribir("sensor/temperatrua") { … }` compila y se ejecuta sin quejarse… y nunca recibe nada. Dos defensas: **constantes** para los nombres de topic, o **tipos** (`enum` / `sealed class`) como clave, como en el `Despachador` de la Clase 2.

**Preguntas**

**17.** ¿En qué esquema detecta el compilador que mandas un `String` donde se esperaba un `Double`?

   a) En el bus pub/sub, gracias al topic
   b) En el Observer, porque la interfaz fija el tipo de `actualizar()`
   c) En ninguno de los dos
   d) En los dos por igual

**18.** ¿En qué situaciones encaja mejor pub/sub que Observer?

   a) Módulos que no deben conocerse entre sí
   b) Un único sujeto y dos observadores en la misma clase
   c) Productores y consumidores en procesos o máquinas distintas
   d) Muchos productores y muchos consumidores

**19.** ¿Qué evita que un topic mal escrito pase desapercibido?

   a) Definir los nombres de topic como constantes
   b) Usar un `enum` o una `sealed class` como clave del bus
   c) Publicar dos veces cada mensaje
   d) Recorrer una copia de la lista de suscriptores

### La GUI como observador: Observable<T> y el ViewModel · Teoría

En `pde-plantilla` (vistas clásicas, TRAB U1) el `onClick` hace **dos trabajos**:

```kotlin
botonPulsa.setOnClickListener {
    pulsaciones++            // 1. cambia el estado (regla de negocio)
    actualizarInterfaz()     // 2. repinta a mano
}

private fun actualizarInterfaz() {
    textPulsaciones.text = "Pulsaciones: $pulsaciones"
}
```

`pulsaciones++` es lógica que debería poder **probarse sin pantalla**; `actualizarInterfaz()` conoce el `TextView` y crece una línea con cada vista nueva. **Objetivo**: que el estado sea un **sujeto** y cada trozo de pantalla un **observador**. Así el listener del botón solo hace un trabajo: avisar al modelo.

```kotlin
class Observable<T>(valorInicial: T) {     // LiveData en miniatura
    private val observadores = mutableListOf<(T) -> Unit>()

    var valor: T = valorInicial
        set(nuevo) {
            field = nuevo
            for (o in observadores.toList()) o(nuevo)
        }

    fun observar(o: (T) -> Unit) {
        observadores += o
        o(valor)                            // valor actual al suscribirse
    }
}

class ContadorViewModel {                  // estado y reglas; sin pantallas
    val pulsaciones = Observable(0)
    val activo = Observable(true)

    fun onPulsar() { if (activo.valor) pulsaciones.valor++ }
    fun onCambiarActivo(v: Boolean) { activo.valor = v }
}
```

<div class="cards">
  <div class="card"><h4>1 · El setter notifica</h4><p>Cambiar <code>valor</code> es el <code>medir()</code> del termómetro: guarda y avisa. El sujeto ahora es genérico.</p></div>
  <div class="card"><h4>2 · Valor actual al suscribirse</h4><p><code>o(valor)</code> en <code>observar()</code>: una vista que llega tarde se pinta bien. LiveData hace exactamente esto.</p></div>
  <div class="card"><h4>3 · Reglas sin pantalla</h4><p><code>onPulsar()</code> decide si cuenta. Se prueba creando el ViewModel y leyendo <code>pulsaciones.valor</code>.</p></div>
</div>

**Preguntas**

**20.** ¿Cuáles son los dos trabajos que mezcla el `onClick` de `pde-plantilla`?

   a) Registrar el listener y darlo de baja
   b) Cambiar el estado y repintar la interfaz a mano
   c) Leer el sensor y escribir en el log
   d) Crear la Activity y destruirla

**21.** ¿Para qué sirve la línea `o(valor)` dentro de `observar()`?

   a) Para que la vista que se suscribe se pinte enseguida con el valor actual
   b) Para dar de baja al observador
   c) Para evitar notificar si no hay cambio
   d) Para copiar la lista de observadores

### Las vistas se suscriben · Ejercicio

Tres «vistas» sobre el mismo estado, añadidas **sin abrir el ViewModel**. Hoy son `println`; mañana, un `TextView` o un Composable.

```kotlin
fun main() {
    val vm = ContadorViewModel()

    vm.pulsaciones.observar { println("[TextView] Pulsaciones: $it") }
    vm.pulsaciones.observar { if (it >= 3) println("[Toast] ¡tres ya!") }
    vm.activo.observar      { println("[Button] enabled = $it") }

    vm.onPulsar(); vm.onPulsar(); vm.onPulsar()
    vm.onCambiarActivo(false)
    vm.onPulsar()            // no cuenta: la regla vive en el ViewModel
}
```

Traza con cuidado:

1. Cada `observar` llama **inmediatamente** al observador con el valor actual.
2. Cada `pulsaciones.valor++` pasa por el setter y avisa a **los dos** observadores de `pulsaciones`, en orden de alta.
3. ¿Qué hace `onPulsar()` cuando `activo.valor` es `false`?

> [!TRUCO] Fíjate en la primera línea
> «Pulsaciones: 0» aparece **antes** de pulsar nada. Es `o(valor)` al suscribirse: la vista que llega tarde también se pinta bien.

**Preguntas**

**22.** Ordena la salida completa del programa.

   Ordena: [Button] enabled = false · [Button] enabled = true · [TextView] Pulsaciones: 0 · [TextView] Pulsaciones: 1 · [TextView] Pulsaciones: 2 · [TextView] Pulsaciones: 3 · [Toast] ¡tres ya!

**23.** ¿Qué imprime el último `vm.onPulsar()`?

   a) `[TextView] Pulsaciones: 4`
   b) `[Toast] ¡tres ya!` otra vez
   c) Nada: `activo` es `false` y el ViewModel no cuenta la pulsación
   d) `[Button] enabled = false` otra vez

### MVVM en JavaScript y los nombres: MVC y MVVM · Teoría

El mismo mini-MVVM en el navegador. Las vistas son `span.textContent` y `button.disabled`; el ViewModel no cambia:

```js
class Observable {                              // LiveData en miniatura
  #observadores = []; #valor;
  constructor(inicial) { this.#valor = inicial; }
  get valor() { return this.#valor; }
  set valor(nuevo) { this.#valor = nuevo; for (const o of [...this.#observadores]) o(nuevo); }
  observar(o) { this.#observadores.push(o); o(this.#valor); }
}

class ContadorViewModel {                       // nada de DOM aquí
  pulsaciones = new Observable(0);
  activo = new Observable(true);
  onPulsar() { if (this.activo.valor) this.pulsaciones.valor++; }
  onCambiarActivo(v) { this.activo.valor = v; }
}

const vm = new ContadorViewModel();
vm.pulsaciones.observar(n => span.textContent = `Pulsaciones: ${n}`);   // vista 1
vm.pulsaciones.observar(n => { if (n >= 3) mostrarToast("¡tres ya!"); }); // vista 2
vm.activo.observar(a => boton.disabled = !a);                             // vista 3
boton.addEventListener("click", () => vm.onPulsar());                    // el listener solo avisa
```

La vista es **una línea de binding**, sin lógica; el listener del clic **solo avisa** al ViewModel.

![A la izquierda, las vistas: listener del clic, span con Pulsaciones, toast y botón disabled. A la derecha, ContadorViewModel con onPulsar() y dos Observable, pulsaciones y activo. El listener del clic llama a vm.onPulsar(), que hace valor++ en pulsaciones. El span y el toast se suscriben a pulsaciones con observar, y el botón a activo. Todas las flechas van de la vista al ViewModel: al cambiar un valor, Observable llama a las lambdas que cada vista le entregó.](/img/pde/tema-2/2a-mvvm.svg "MVVM: la vista llama y se suscribe; el ViewModel no conoce ninguna vista")

| Pieza de hoy | MVC (Smalltalk, 1979) | MVVM (Android, .NET, Vue) | En Android real |
| --- | --- | --- | --- |
| `ContadorViewModel` + `Observable` | Modelo (sujeto) | ViewModel expone estado observable | `ViewModel` + `LiveData` / `StateFlow` |
| `println` / `TextView` | Vista (observador) | La vista se suscribe: *binding* | `observe(owner) { }` · `collectAsState()` |
| `onPulsar()`, `onCambiarActivo()` | Controlador: recibe la entrada, modifica el modelo | La vista llama métodos del ViewModel | `setOnClickListener { vm.onPulsar() }` |

> [!IMPORTANTE] Lo que no cambia
> En los dos, **el modelo es el sujeto y la vista un observador**. La vista nunca es la dueña del estado.

> [!NOTA] Sesión 0 · Compose
> En Compose no escribíais `actualizarInterfaz()`: la **recomposición** es esto mismo hecho por el framework. `collectAsState()` es `observar()`.

**Preguntas**

**24.** En MVC, ¿qué papel hace la pieza que recibe la entrada del usuario y modifica el modelo?

   a) Vista
   b) Modelo
   c) Controlador
   d) Observable

**25.** En Jetpack Compose, ¿qué equivale a nuestro `observar()`?

   a) `setContentView()`
   b) `collectAsState()`
   c) `findViewById()`
   d) `onCreate()`

### La escalera de la Unidad 2 · Ejercicio

Cinco nombres, **una sola idea**: cada peldaño añade una pieza.

<div class="flow">
  <div><b>1 · Callback</b><small>la fuente recibe una función · Clase 1</small></div>
  <div><b>2 · Listener</b><small>varias funciones, alta y baja · Clase 1, Sensor</small></div>
  <div><b>3 · Observer (GoF)</b><small>sujeto con estado + contrato update() · hoy</small></div>
  <div><b>4 · Pub/sub · bus</b><small>nadie conoce a nadie; un topic en medio · Clase 2 (Despachador) y hoy</small></div>
  <div><b>5 · MVC / MVVM</b><small>el modelo es el sujeto; la pantalla, un observador más · hoy</small></div>
</div>

Idea de la clase: **Sensor era un Observer, Despachador era un bus. Una vista es un observador más.**

### Criterio de elección

| Usa… | Cuando… |
| --- | --- |
| Callback | Una operación puntual. |
| Observer | Estado claro, pocos interesados, mismo código. |
| Bus | Módulos que no deben conocerse o viven en sitios distintos. |
| MVVM | Siempre que haya pantalla. |

Cuanto más subes, **más desacoplo y menos compilador** (menos comprobaciones en tiempo de compilación).

Aplica el criterio a los casos de las preguntas.

**Preguntas**

**26.** Ordena la escalera de la Unidad 2, del peldaño más bajo al más alto.

   Ordena: Callback · Listener · MVC / MVVM · Observer (GoF) · Pub/sub (bus)

**27.** Quieres que te avisen **una vez** cuando termine de descargarse un fichero. ¿Qué peldaño basta?

   a) MVVM
   b) Pub/sub con un broker
   c) Callback
   d) Observer con tres observadores

**28.** El módulo de pagos debe avisar al de correo y al de estadísticas, que lo mantienen equipos distintos y no deben depender de él. ¿Qué eliges?

   a) Un bus pub/sub con topics
   b) Un callback
   c) Un `when` en el módulo de pagos
   d) Un `Observable` dentro de una Activity

### Ampliación: invariantes y riesgos durante el despacho · Teoría

> [!NOTA] Contenido de ampliación (stretch)
> Va más allá de los obligatorios, pero ayuda a responder bien las preguntas de «qué puede salir mal».

### Invariantes de una notificación

<div class="cards">
  <div class="card"><h4>Baja efectiva</h4><p>Una suscripción retirada no recibe entregas futuras, según el contrato.</p></div>
  <div class="card"><h4>Registro íntegro</h4><p>Un handler no corrompe la lista al registrar o retirar otro (de ahí la ronda sobre una copia).</p></div>
  <div class="card"><h4>Datos suficientes</h4><p>El evento transporta lo necesario para procesarlo sin consultar al emisor.</p></div>
</div>

### Riesgos durante el despacho

- **Reentrancia**: un handler provoca otro despacho antes de que acabe el primero.
- **Cambios en notify**: altas y bajas durante la iteración alteran quién recibe el evento.
- **Ciclos**: A avisa a B y B vuelve a avisar a A sin condición de parada.

### Costes de un callback mal gestionado

- **Lentitud**: el despacho síncrono retrasa a todos los receptores posteriores.
- **Referencia obsoleta**: el callback sobrevive a la pantalla, tarea o dispositivo que lo creó.
- **Anidación**: varias dependencias anidadas dispersan errores y cancelación.

### Traza: un observador que toca el sujeto desde dentro

```kotlin
val temp = Observable(20)
temp.observar { if (it > 40) temp.valor = 40 }   // A: «corrige» desde dentro
temp.observar { println("Pantalla: $it") }        // B
temp.valor = 45
println("valor final: ${temp.valor}")
```

Al poner 45, el setter recorre [A, B] con `nuevo = 45`. A, **antes de que B se entere**, pone 40: se abre una **segunda ronda** completa (A con 40, B con 40). Al volver, la primera ronda continúa y entrega 45 a B. Resultado: la pantalla muestra **45** pero el valor real es **40**.

**Preguntas**

**29.** Ordena la salida de la traza.

   Ordena: Pantalla: 20 · Pantalla: 40 · Pantalla: 45 · valor final: 40

**30.** Con difusión sobre una copia, el observador A da de baja a B **durante** una notificación, y B va después de A. ¿Recibe B este evento?

   a) No, nunca más recibe nada, ni este evento
   b) Sí recibe este (está en la copia de la ronda), pero no los siguientes
   c) Recibe este evento dos veces
   d) Se lanza `ConcurrentModificationException`

### Repaso: tres preguntas de parcial y puente a la U3 · Repaso

> [!RESUMEN] Chuleta
> - **Observer (GoF)**: sujeto con estado + lista; attach / detach / notify / update. Invariantes: solo el sujeto cambia su estado, sin cambio no hay aviso, el observador no pregunta.
> - **Push** (dato pequeño, todos lo quieren) frente a **pull** (estado grande, cada uno coge su parte).
> - **Pub/sub**: un bus con topics; nadie conoce a nadie; `suscribir` devuelve la baja; si nadie escucha, se pierde. Más desacoplo, menos compilador.
> - **MVVM**: el ViewModel es el sujeto (estado + reglas, sin pantalla); las vistas observan; el listener del botón solo avisa. `Observable<T>` entrega el valor actual al suscribirse.
> - **Escalera**: callback → listener → Observer → pub/sub → MVC/MVVM.

> [!EXAMEN] Las tres preguntas que pueden caer en el parcial 1
> 1. ¿Qué invariantes rompe un observador que **modifica el estado del sujeto desde dentro de `actualizar()`**? ¿Qué les pasa a los demás observadores?
> 2. **Un topic mal escrito no da error.** Propón dos formas de evitarlo en un bus.
> 3. **Reescribe `MainActivity` con `ContadorViewModel`.** Explica qué línea desaparece de `pde-plantilla` y por qué.

<details><summary>Ideas para responderlas</summary>
<p>1. Rompe «solo el sujeto cambia su estado» y provoca <b>reentrancia</b>: se abre una notificación dentro de otra, los demás reciben avisos anidados y desordenados y pueden quedarse con un valor obsoleto (la traza del paso anterior).</p>
<p>2. Constantes para los nombres de topic; o tipos (<code>enum</code> / <code>sealed class</code>) como clave, como en el <code>Despachador</code>.</p>
<p>3. Desaparece la llamada a <code>actualizarInterfaz()</code> (y la función): el <code>TextView</code> observa <code>vm.pulsaciones</code> y se repinta solo. El listener queda en <code>setOnClickListener { vm.onPulsar() }</code>.</p>
</details>

### Puente a la Unidad 3

**Hoy todo ha sido síncrono**: `publicar()` recorre la lista y vuelve. En la U3 metemos una **cola** entre el que publica y el que escucha: bucle de eventos, orden, prioridades… y el handler lento que congela la pantalla.

**Preguntas**

**31.** Al reescribir `MainActivity` con `ContadorViewModel`, ¿qué desaparece del listener del botón?

   a) `setOnClickListener`
   b) La llamada a `actualizarInterfaz()`: la vista observa el estado y se repinta sola
   c) `vm.onPulsar()`
   d) Nada, queda igual

**32.** Marca las afirmaciones **verdaderas**.

   a) En pub/sub, si nadie está suscrito a un topic, el publicador recibe un error
   b) En Observer, el sujeto es quien guarda la lista de observadores
   c) `Observable<T>` llama al observador con el valor actual en cuanto se suscribe
   d) En MVVM la vista es la dueña del estado
   e) Un observador que modifica el sujeto dentro de `actualizar()` provoca reentrancia

**33.** ¿Qué estructura se colocará en la U3 entre quien publica y quien escucha? (una palabra)

### Solucionario

1. b) Gang of Four: los cuatro autores del libro *Design Patterns* (1994)  
   *Gang of Four*: Gamma, Helm, Johnson y Vlissides.
2. a) Solo el sujeto cambia su estado; c) Sin cambio, no hay aviso; d) El observador no pregunta: le llaman  
   Las otras dos son justo lo contrario: nada de *polling* y los observadores no se conocen.
3. b) Para que nadie de fuera fuerce avisos: solo el sujeto decide notificar cuando su estado cambia  
   Si fuese pública, cualquiera podría provocar avisos sin cambio real y se rompería «sin cambio, no hay aviso».
4. `private set`  
   `private set` en la propiedad: el getter es público, el setter privado. Invariante 1.
5. 1. Pantalla: 22.5 ºC → 2. Pantalla: 31.0 ºC → 3. ALARMA: 31.0 > 30.0  
   22.5 notifica (Pantalla imprime; Alarma no salta; Registro guarda en silencio). El segundo 22.5 no notifica. 31.0 notifica: Pantalla y luego Alarma.
6. b) Porque `medir()` hace `return` si el valor no cambia: sin cambio no hay aviso  
   La comprobación vive en el **sujeto**, no en cada observador: `if (nueva == temperatura) return`.
7. c) [22.5, 31.0]  
   Solo recibe las dos notificaciones reales: 22.5 y 31.0.
8. c) Al notificar: `o.actualizar` no es una función y salta un `TypeError` en ejecución  
   Sin interfaz, nadie comprueba el contrato hasta que se intenta llamar a `actualizar()`.
9. b) 1  
   La baja compara por **identidad** (`!==`). Una segunda `new Pantalla()` es otro objeto: no quita nada.
10. b) Push  
   Un `Double` que todos necesitan: se empuja directamente en `actualizar(t)`.
11. b) Pull: se avisa con `actualizar(documento)` y cada observador lee la parte que necesita  
   Con estado grande y necesidades distintas, cada observador **tira** (*pull*) de lo suyo.
12. a) Estado propio; c) La regla «sin cambio, no hay aviso»  
   Lista, alta y baja ya las tenía. Le faltaban **estado** y **notificar solo si cambia**.
13. c) El mensaje se pierde en silencio  
   `topics[topic] ?: return`: sin lista no hay nada que hacer, y nadie se entera. Es una decisión de diseño.
14. b) Una función que, al llamarla, da de baja a ese suscriptor  
   Suscripción cancelable (estrategia 2 de la Clase 2): quien guarda esa función es el dueño de la suscripción.
15. 1. log: 28 → 2. bienvenida a ana → 3. alarma: 35  
   28 → solo log (la alarma no salta). Humedad → silencio. Login → bienvenida. Tras la baja del log, 35 → solo alarma.
16. b) Porque `bajaLog()` quitó ese suscriptor antes de publicar 35  
   `bajaLog` es la función de baja que devolvió `suscribir`. Al invocarla, el log deja de escuchar.
17. b) En el Observer, porque la interfaz fija el tipo de `actualizar()`  
   El bus usa `Any` y un `String` como clave: los errores aparecen en ejecución. El Observer tiene tipado fuerte.
18. a) Módulos que no deben conocerse entre sí; c) Productores y consumidores en procesos o máquinas distintas; d) Muchos productores y muchos consumidores  
   Pocos sujetos y una relación clara en el mismo código: Observer. El bus brilla cuando hace falta desacoplar mucho.
19. a) Definir los nombres de topic como constantes; b) Usar un `enum` o una `sealed class` como clave del bus  
   Con constantes o tipos, un error de escritura se convierte en error de compilación.
20. b) Cambiar el estado y repintar la interfaz a mano  
   Estado y pantalla pegados: `pulsaciones++` y `actualizarInterfaz()` en el mismo sitio.
21. a) Para que la vista que se suscribe se pinte enseguida con el valor actual  
   Una vista que llega tarde no se queda en blanco esperando el próximo cambio. LiveData se comporta igual.
22. 1. [TextView] Pulsaciones: 0 → 2. [Button] enabled = true → 3. [TextView] Pulsaciones: 1 → 4. [TextView] Pulsaciones: 2 → 5. [TextView] Pulsaciones: 3 → 6. [Toast] ¡tres ya! → 7. [Button] enabled = false  
   Suscribirse pinta el valor actual (0 y true; el Toast calla con 0). Tres pulsaciones: 1, 2, 3 y, tras el 3, el Toast. Desactivar avisa al botón. La última pulsación no cuenta.
23. c) Nada: `activo` es `false` y el ViewModel no cuenta la pulsación  
   La regla («si no está activo, no cuenta») vive en el ViewModel y se puede probar sin pantalla.
24. c) Controlador  
   El **controlador**. En MVVM ese papel lo cubren los métodos del ViewModel (`onPulsar()`), a los que llama la vista.
25. b) `collectAsState()`  
   `collectAsState()` suscribe el Composable al estado; cada cambio provoca una **recomposición**, que es el repintado automático.
26. 1. Callback → 2. Listener → 3. Observer (GoF) → 4. Pub/sub (bus) → 5. MVC / MVVM  
   Callback → listener → Observer → pub/sub → MVC/MVVM. Cada peldaño añade una pieza a la anterior.
27. c) Callback  
   Operación puntual: un callback como el `descargar(url, alTerminar)` de la Clase 1.
28. a) Un bus pub/sub con topics  
   Módulos que no deben conocerse: el intermediario (bus) desacopla al publicador de sus consumidores.
29. 1. Pantalla: 20 → 2. Pantalla: 40 → 3. Pantalla: 45 → 4. valor final: 40  
   La ronda anidada (40) se completa antes de que la exterior entregue 45 a B. B se queda con un valor **obsoleto**: eso es la reentrancia.
30. b) Sí recibe este (está en la copia de la ronda), pero no los siguientes  
   La copia es una foto de la ronda actual. La baja es efectiva para **entregas futuras**, como dice la invariante. Si quieres otra política, el contrato debe decirlo.
31. b) La llamada a `actualizarInterfaz()`: la vista observa el estado y se repinta sola  
   El listener pasa a hacer un solo trabajo: `vm.onPulsar()`. El repintado lo hace el observador del `TextView`.
32. b) En Observer, el sujeto es quien guarda la lista de observadores; c) `Observable<T>` llama al observador con el valor actual en cuanto se suscribe; e) Un observador que modifica el sujeto dentro de `actualizar()` provoca reentrancia  
   En pub/sub el mensaje se pierde **en silencio**, y en MVVM el estado es del ViewModel, nunca de la vista.
33. `cola`  
   Una **cola** de eventos: el despacho deja de ser síncrono y aparecen el bucle de eventos, el orden y las prioridades.

## Cuando notificar sale mal

_La letra pequeña de publicar(): orden, altas y bajas en plena ronda, reentrancia y ciclos, antipatrones de suscripción (duplicados, bajas fallidas, callbacks tardíos, excepciones, punteros colgantes) y los errores típicos de Observer, MVP y MVVM._

### Un publicar() tiene letra pequeña · Teoría

En las clases anteriores de esta unidad construimos callbacks, listeners, un sujeto observable y un bus de mensajes. Todo funcionaba por una razón que no dijimos en voz alta: **los suscriptores se portaban bien**. Recibían el aviso, hacían su trabajo y devolvían el control.

Hoy toca lo contrario. Mientras el sujeto recorre su lista, **cada suscriptor puede cambiar el sistema que le está llamando**.

<div class="flow">
  <div><b>Sensor</b><small>publicar(31 °C)</small></div>
  <div><b>Dispatcher</b><small>recorre la lista</small></div>
  <div><b>Pantalla</b><small>turno 1</small></div>
  <div><b>Sirena</b><small>turno 2</small></div>
  <div><b>Historial</b><small>turno 3</small></div>
</div>

### Qué es una ronda

Una **ronda** empieza al entrar en `publicar()` y termina cuando `publicar()` retorna. Si un suscriptor publica durante su turno, empieza una **ronda anidada** antes de que acabe la primera.

Ahora imagina que la Pantalla, durante su turno…

- …da de baja a la Sirena → *orden y cambios en la lista* (bloque 1).
- …vuelve a llamar a `publicar()` → *reentrancia y ciclos* (bloque 2).
- …lanza una excepción o tarda 3 segundos → *antipatrones* (bloque 3).

### Las cuatro preguntas de la letra pequeña

<div class="cards">
  <div class="card"><h4>1 · Orden</h4><p>¿Se garantiza que se avisa en el orden de alta?</p></div>
  <div class="card"><h4>2 · Altas y bajas en la ronda</h4><p>¿Afectan a la ronda en curso o a la siguiente?</p></div>
  <div class="card"><h4>3 · Reentrancia</h4><p>¿Se puede publicar desde un suscriptor? ¿Se entrega ya o se encola?</p></div>
  <div class="card"><h4>4 · Fallos</h4><p>Si un suscriptor lanza, ¿se sigue avisando a los demás?</p></div>
</div>

> [!IMPORTANTE] La idea de la clase
> Dos implementaciones con **la misma lista y el mismo bucle** pueden responder distinto a estas cuatro preguntas. Esas respuestas **son la API**, aunque no aparezcan en la firma de `publicar()`.

> [!ANALOGIA] Pasar lista
> Pasar lista parece trivial hasta que un alumno se va a mitad, otro llega tarde o alguien interrumpe con una pregunta. Lo que ocurra entonces no lo decide la lista, sino las normas de la clase. Con `publicar()` pasa lo mismo: las normas son el contrato.

**Preguntas**

**1.** ¿Cuándo termina una **ronda** de notificación?

   a) Cuando el primer suscriptor devuelve el control
   b) Cuando el sujeto cambia de valor
   c) Cuando `publicar()` retorna
   d) Cuando se vacía la lista de suscriptores

**2.** Marca las cuatro preguntas que forman la «letra pequeña» de `publicar()`.

   a) ¿Se garantiza el orden de alta?
   b) ¿Qué tipo devuelve `publicar()`?
   c) ¿Las altas y bajas afectan a la ronda en curso o a la siguiente?
   d) ¿Se puede publicar desde un suscriptor?
   e) ¿Cómo se llama el topic?
   f) Si un suscriptor lanza, ¿se sigue avisando a los demás?

### Caso: el envío gratis que no aparece · Ejercicio

Tienda web. Dos módulos se suscriben al **mismo evento** del bus. `banner.js` se carga antes que `resumen.js`, así que su suscriptor se registra primero. La tienda regala el envío a partir de 50 €.

```js
// banner.js (se carga primero)
bus.suscribir("carrito/anadido", (item) => {
  if (resumen.total >= 50) banner.mostrar("¡Envío gratis!");
});

// resumen.js (se carga después)
bus.suscribir("carrito/anadido", (item) => {
  resumen.total += item.precio;
});

// El carrito lleva 45 €; se añaden unas zapatillas de 10 €
bus.publicar("carrito/anadido", { nombre: "Zapatillas", precio: 10 });
```

Antes de seguir, **predice**: recorre la ronda suscriptor por suscriptor y apunta cuánto vale `resumen.total` en cada momento.

> [!TRUCO] Cómo predecir una ronda
> Escribe en una columna los suscriptores en orden de alta. Para cada uno, anota qué **lee** y qué **escribe**. El fallo suele estar donde alguien lee algo que otro todavía no ha escrito.

**Preguntas**

**3.** ¿Qué ve el usuario después de añadir las zapatillas?

   a) El banner «¡Envío gratis!»
   b) Nada: el total ya es 55 € y no hay banner
   c) El banner aparece dos veces
   d) Un error porque `resumen` todavía no existe

**4.** Si alguien reordena los `<script>` y `resumen.js` pasa a cargarse primero, ¿qué ocurre?

   a) Sigue sin aparecer el banner
   b) El bus lanza una excepción por suscriptores duplicados
   c) Aparece el banner: el fallo desaparece… hasta que otro cambio de orden lo traiga de vuelta
   d) El banner aparece antes de añadir las zapatillas

### Estado derivado en el sujeto · Teoría

El banner no falla por un error de sintaxis, sino por una **dependencia oculta**: necesita un resultado que calcula *otro suscriptor del mismo evento*.

<div class="cards">
  <div class="card"><h4>Lee un estado que aún no existe</h4><p>El banner consulta <code>resumen.total</code> antes de que se actualice.</p></div>
  <div class="card"><h4>El orden lo decide el &lt;script&gt;</h4><p>Si se reordenan los imports, el fallo desaparece… o aparece en otro sitio.</p></div>
  <div class="card"><h4>Nadie lo ha prometido</h4><p>Si la API no garantiza el orden (pregunta 1), depender de él es apostar.</p></div>
</div>

### Solución: el sujeto calcula y luego avisa

```js
// carrito.js
class Carrito {
  #total = 0;
  anadir(item) {
    this.#total += item.precio;            // primero el estado
    bus.publicar("carrito/cambiado",
      { item, total: this.#total });       // después el aviso
  }
}

bus.suscribir("carrito/cambiado", ({ total }) => {
  if (total >= 50) banner.mostrar("¡Envío gratis!");
});
```

El total pasa a ser **estado derivado del sujeto**: el carrito lo calcula antes de avisar y el evento ya lleva el valor correcto. Nadie depende de nadie, y el orden de carga deja de importar.

> [!IMPORTANTE] Regla
> Los suscriptores de un mismo evento deben ser **independientes entre sí**. Si B necesita el resultado de A, entonces A es parte del sujeto, o A publica su propio evento (por ejemplo `carrito/total-cambiado`) y B se suscribe a ese.

> [!NOTA] Invariante «datos suficientes»
> El evento transporta lo necesario para procesarlo **sin consultar al emisor** ni a otros módulos. Si un suscriptor tiene que ir a mirar el estado de otro, ya depende de cuándo lo mire.

> [!EXAMEN]
> «Primero el estado, después el aviso». Un sujeto que avisa antes de terminar de actualizarse, o que deja que los suscriptores calculen lo que él debería saber, genera fallos que dependen del orden.

**Preguntas**

**5.** ¿Cuál es el arreglo que propone el material para el envío gratis?

   a) Garantizar con un comentario que `banner.js` se cargue siempre después de `resumen.js`
   b) Que el banner espere 100 ms con `setTimeout` antes de leer el total
   c) Que el banner se suscriba dos veces al evento
   d) Que el carrito actualice su total y publique `carrito/cambiado` con el total ya calculado

**6.** En la solución, ¿qué campo del evento `carrito/cambiado` lee el banner para decidir? (una palabra)

### Caso: la valoración que nunca se pide · Ejercicio

App de reparto. Un `Pedido` avisa de sus cambios de estado a tres suscriptores: el **mapa** (solo le interesa el pedido hasta que se entrega, así que se da de baja él solo), la **petición de valoración** y el **historial**.

```js
class Pedido {
  #oyentes = [];
  suscribir(f) {
    this.#oyentes.push(f);
    return () => this.#oyentes.splice(this.#oyentes.indexOf(f), 1);
  }
  cambiarEstado(estado) {
    this.#oyentes.forEach((f) => f(estado)); // lista VIVA
  }
}

const pedido = new Pedido();

const bajaMapa = pedido.suscribir((e) => {
  console.log(`Mapa: ${e}`);
  if (e === "entregado") bajaMapa();       // un solo uso
});

pedido.suscribir((e) => {
  if (e === "entregado") console.log("¿Qué tal tu pedido?");
});

pedido.suscribir((e) => console.log(`Historial: ${e}`));

pedido.cambiarEstado("entregado");
```

Dos detalles: `suscribir` devuelve la función de baja (como vimos en la clase 2), y `cambiarEstado` recorre **la propia lista** `#oyentes`, no una copia. `forEach` avanza por índices: 0, 1, 2…

Dibuja el array antes y después del `splice` y predice la salida.

**Preguntas**

**7.** ¿Qué líneas aparecen en la consola?

   a) `Mapa: entregado`
   b) `¿Qué tal tu pedido?`
   c) `Historial: entregado`
   d) Un `TypeError` por modificar el array durante `forEach`

**8.** Justo después de la baja del Mapa, ¿en qué posición queda Valoración y por qué índice sigue el bucle?

   a) Valoración en la posición 1; el bucle va a la 1
   b) Valoración en la posición 2; el bucle vuelve a la 0
   c) Valoración en la posición 0; el bucle pasa a la 1
   d) Valoración desaparece del array junto con el Mapa

### Recorrer una foto de la lista · Teoría

Lo que ha pasado, índice a índice:

| Momento | Posición 0 | Posición 1 | Posición 2 | Bucle |
| --- | --- | --- | --- | --- |
| Antes de la baja | Mapa | Valoración | Historial | i = 0 → Mapa |
| Tras la baja | Valoración | Historial | — | i = 1 → Historial |

Valoración pasa a la posición 0 cuando el bucle ya va por la 1. Se la salta **sin avisar**: no hay excepción, no hay mensaje, simplemente un suscriptor que no se entera. Es el peor tipo de fallo, porque solo lo detecta quien echa de menos la línea.

### Solución: recorrer una foto

```js
cambiarEstado(estado) {
  [...this.#oyentes].forEach((f) => f(estado)); // copia
} // Mapa · ¿Qué tal tu pedido? · Historial
```

La ronda recorre **una copia** de la lista tal y como estaba al empezar. Las altas y bajas modifican la lista real, no la foto, así que el bucle no se descoloca.

![A la izquierda, la lista viva: en el turno i = 0 el Mapa hace splice(0, 1), Valoración baja a la posición 0 e Historial a la 1; el bucle pasa a i = 1, llama a Historial y Valoración se queda sin turno. A la derecha, la foto: la ronda recorre la copia con Mapa, Valoración e Historial en las posiciones 0, 1 y 2 y avisa a los tres, mientras la baja solo cambia la lista real](/img/pde/tema-2/2b-lista-viva-foto.svg "Lista viva frente a foto: con splice los índices se desplazan y el bucle se salta a Valoración; la copia no se mueve")

| Lenguaje | Cómo se hace la foto |
| --- | --- |
| JavaScript | `[...listeners]` |
| Kotlin | `oyentes.toList()` |
| C++ | `auto foto = handlers_;` |

> [!CUIDADO] En Kotlin el fallo no es silencioso
> `for (f in oyentes)` con una baja dentro lanza `ConcurrentModificationException`. **Un fallo que se ve siempre es mejor que un suscriptor que desaparece en silencio.**

> [!NOTA] Invariante «registro íntegro»
> Un handler no debe corromper la lista al registrar o retirar a otro. La foto es la forma más sencilla de garantizarlo.

**Preguntas**

**9.** ¿Qué excepción lanza Kotlin si se da de baja un suscriptor dentro de `for (f in oyentes)`?

**10.** ¿Por qué el material considera preferible el comportamiento de Kotlin al de JavaScript en este caso?

   a) Porque un fallo que se ve siempre es mejor que un suscriptor que desaparece en silencio
   b) Porque Kotlin es más rápido recorriendo listas
   c) Porque en Kotlin la baja no se ejecuta nunca
   d) Porque Kotlin hace la foto automáticamente

### Caso: «Ha llegado a su destino. Gire a la derecha» · Ejercicio

App de navegación en Kotlin. Aprendida la lección anterior, el `Gps` **ya recorre una foto** de su lista (`subs.toList()`), así que nadie rompe el bucle. Hay dos suscriptores, dados de alta en este orden: `controlRuta` y `voz`.

```kotlin
// suscritos en este orden: controlRuta, voz
val controlRuta = gps.suscribir { pos ->
    if (pos == destino) {
        println("Ha llegado a su destino")
        gps.cancelar(voz)                    // se apaga la voz…
    }
}

val voz = gps.suscribir { pos ->
    println(ruta.instruccionPara(pos))       // …pero estaba en la foto
}
```

*(Fragmento tal y como se presenta en clase: lo que importa es el orden de alta y que `controlRuta` cancela a `voz` durante su turno.)*

El coche llega al destino y el `Gps` publica esa posición. `controlRuta` tiene el primer turno y cancela la voz. Para esa posición, la instrucción de ruta sería «En 200 m, gire a la derecha».

Piensa en qué contiene la foto **en el momento en que empezó la ronda**.

**Preguntas**

**11.** ¿Qué se imprime?

   a) Solo «Ha llegado a su destino»
   b) «En 200 m, gire a la derecha» y después «Ha llegado a su destino»
   c) Una `ConcurrentModificationException`
   d) «Ha llegado a su destino» y después «En 200 m, gire a la derecha»

**12.** ¿Por qué aquí no salta `ConcurrentModificationException` aunque se modifique la lista durante la ronda?

   a) Porque el bucle recorre una copia (`toList()`), no la lista que se modifica
   b) Porque `cancelar` no modifica la lista
   c) Porque esa excepción solo existe en Java, no en Kotlin
   d) Porque `voz` todavía no estaba suscrita

### Foto pura o foto + comprobar si sigue activa · Teoría

La foto arregla el bucle, pero un suscriptor dado de baja **durante** la ronda aún recibe el evento en curso. Si eso no es lo que queremos, se añade una comprobación: cada suscripción lleva una bandera `activa`.

```kotlin
class Suscripcion(val f: (Posicion) -> Unit) { var activa = true }

class Gps {
    private val subs = mutableListOf<Suscripcion>()

    fun suscribir(f: (Posicion) -> Unit) =
        Suscripcion(f).also { subs += it }

    fun cancelar(s: Suscripcion) { s.activa = false; subs -= s }

    fun publicar(p: Posicion) {
        for (s in subs.toList())   // foto: nadie rompe el bucle
            if (s.activa) s.f(p)   // una baja cuenta desde ya
    }
}
```

La foto sigue conteniendo a `voz`, pero su suscripción ya está marcada como inactiva y el bucle la salta. Salida: solo «Ha llegado a su destino».

### Dos políticas válidas

|  | Foto pura | Foto + comprobar activa |
| --- | --- | --- |
| Baja durante la ronda | Recibe el evento en curso | No lo recibe |
| Alta durante la ronda | Espera a la siguiente | Espera a la siguiente |
| Quién lo hace así | Node `EventEmitter`, nuestro Bus | DOM `addEventListener` / `removeEventListener` |

> [!IMPORTANTE] Regla
> Ninguna política es «la correcta»; **lo incorrecto es no saber cuál ofrece vuestra API**. En ambas, un alta durante la ronda espera a la siguiente.

> [!NOTA] Invariante «baja efectiva»
> Una suscripción retirada no recibe entregas *futuras* **según el contrato**. La foto pura cumple el invariante si su contrato dice que la ronda en curso no cuenta como futura; la del DOM, si dice que sí.

> [!EXAMEN]
> «¿Quién recibe un evento si un handler se da de baja durante la notificación?» La respuesta correcta empieza por «depende de la política»: explica las dos y di cuál usa la plataforma del enunciado.

**Preguntas**

**13.** En el DOM, un listener que se quita con `removeEventListener` durante la ronda de un evento…

   a) …recibe igualmente el evento en curso
   b) …no recibe el evento en curso
   c) …provoca que se cancele la ronda entera
   d) …lanza una excepción en el siguiente listener

**14.** Durante una ronda, un suscriptor da de alta a otro nuevo. Con cualquiera de las dos políticas, el nuevo…

   a) …recibe el evento en curso al final de la ronda
   b) …recibe el evento en curso inmediatamente
   c) …espera a la siguiente ronda
   d) …no recibe nunca nada hasta que se reinicie el sujeto

**15.** ¿Cómo se llama la bandera de `Suscripcion` que comprueba el bucle antes de llamar?

### Caso: el carrito que muestra 12 cuando hay 10 · Ejercicio

Bloque 2: **reentrancia**. Usamos el `Observable` de la clase 3, sin aviso inicial al suscribirse. Ya recorre una foto. Hay 10 unidades en stock; un observador **limita** la cantidad al stock y otro la **pinta**. El usuario escribe 12.

```kotlin
class Observable<T>(inicial: T) {
    private val observadores = mutableListOf<(T) -> Unit>()
    var valor: T = inicial
        set(nuevo) {
            if (nuevo == field) return
            field = nuevo
            for (o in observadores.toList()) o(nuevo)
        }
    fun observar(o: (T) -> Unit) { observadores += o }
}

fun main() {
    val stock = 10
    val cantidad = Observable(1)
    cantidad.observar { if (it > stock) cantidad.valor = stock } // limitar
    cantidad.observar { println("  [Carrito] cantidad: $it") }   // pintar
    cantidad.valor = 12                                          // el usuario escribe 12
    println("  valor real: ${cantidad.valor}")
}
```

Pista de lectura: `cantidad.valor = stock` no es una simple asignación. Es una llamada al `set`, que tiene **su propio `for`**. ¿Termina ese `for` antes o después de que el primero llegue a `pintar`? ¿Y qué valor lleva cada `for` en su variable `nuevo`?

**Preguntas**

**16.** Ordena las líneas de la salida del programa.

   Ordena: [Carrito] cantidad: 10 · [Carrito] cantidad: 12 · valor real: 10

**17.** ¿Qué invariante se rompe?

   a) Que lo último que pinta la vista coincida con el valor real del modelo
   b) Que el stock nunca baje de cero
   c) Que cada observador se suscriba una sola vez
   d) Que el valor inicial sea 1

### Rondas anidadas y dos arreglos · Teoría

Un observador que cambia el sujeto **en su turno** abre una ronda anidada. La traza:

```text
ronda 1 · valor = 12
  ① limitar(12) → valor = 10
      ronda 2 · valor = 10
        ① limitar(10) · nada
        ② pintar(10) → «cantidad: 10»
  ② pintar(12) → «cantidad: 12»
  ↑ lo último que se pinta es el valor viejo
```

La ronda 1 no se entera de que el valor ha cambiado: sigue repartiendo el 12 que tenía en `nuevo`. Todo ocurre en **un solo hilo**: la reentrancia es una llamada dentro de otra.

### A · La regla vive en el sujeto (preferida)

```kotlin
class CarritoViewModel(private val stock: Int) {
    val cantidad = Observable(1)
    fun onCambiarCantidad(n: Int) {
        cantidad.valor = n.coerceIn(1, stock)
    }
}
// Los observadores solo pintan
// SALIDA A:  [Carrito] cantidad: 10
```

Que el observador no tenga que corregir al sujeto: la validación forma parte del sujeto (`coerceIn` recorta al intervalo 1..stock) y el valor inválido nunca llega a publicarse.

### B · Entrega diferida: las rondas no se anidan

```kotlin
class Observable<T>(inicial: T) {
    private val observadores = mutableListOf<(T) -> Unit>()
    private val pendientes = ArrayDeque<T>()       // cola de avisos
    private var notificando = false
    var valor: T = inicial
        set(nuevo) {
            if (nuevo == field) return
            field = nuevo
            pendientes.addLast(nuevo)
            if (notificando) return                  // reentrada: encola y vuelve
            notificando = true
            try {
                while (pendientes.isNotEmpty()) {
                    val v = pendientes.removeFirst()
                    for (o in observadores.toList()) o(v)
                }
            } finally { notificando = false }
        }
    fun observar(o: (T) -> Unit) { observadores += o }
}
// SALIDA B:  [Carrito] cantidad: 12
//            [Carrito] cantidad: 10   ← valor real
//            valor real: 10
```

Si no se puede evitar que un observador publique, que **la ronda anidada espere**: cada ronda termina antes de que empiece la siguiente. En la Unidad 3 esto tiene nombre: **run-to-completion**. Con B se ve un instante el 12; por eso A va primero.

![A la izquierda, sin cola: dentro de la ronda 1 (nuevo = 12), limitar pone el valor a 10 y abre la ronda 2, que se completa entera y pinta 10; al volver, la ronda 1 pinta 12, el valor viejo, y la pantalla se queda en 12 con el valor real en 10. A la derecha, con cola: limitar deja el 10 en pendientes, la ronda 1 termina pintando 12 un instante y después la ronda 2 pinta 10, que coincide con el valor real](/img/pde/tema-2/2b-rondas-anidadas-cola.svg "El carrito sin cola (rondas anidadas) y con la cola del arreglo B (una ronda tras otra)")

**Preguntas**

**18.** ¿Por qué el arreglo A (regla en el sujeto) se prefiere al B (cola de avisos)?

   a) Porque B no compila en Kotlin
   b) Porque A usa menos memoria al no tener observadores
   c) Porque B anida todavía más rondas
   d) Porque con A el valor inválido nunca se publica; con B la vista llega a mostrar el 12 un instante

**19.** ¿Cómo se llama (en inglés) la propiedad de que cada ronda termine antes de que empiece la siguiente?

**20.** En el arreglo B, cuando `limitar` asigna 10 durante la ronda, ¿qué hace el `set`?

   a) Abre una ronda anidada como antes
   b) Descarta el 10 porque ya se está notificando
   c) Guarda el 10 en `pendientes` y retorna, porque `notificando` es `true`
   d) Lanza una excepción por reentrada

### Caso: el tema oscuro que rebota · Ejercicio

App de móvil con smartwatch. Cuando el usuario cambia el tema, cada módulo lo aplica y, para que «el otro se entere», **avisa de que ya lo tiene** por el mismo topic.

```js
// Módulo Reloj
bus.suscribir("ajustes/tema", (tema) => {
  reloj.aplicarTema(tema);
  bus.publicar("ajustes/tema", tema); // reenvía por el mismo topic
});

// Módulo Móvil
bus.suscribir("ajustes/tema", (tema) => {
  movil.aplicarTema(tema);
  bus.publicar("ajustes/tema", tema);
});

bus.publicar("ajustes/tema", "oscuro");
```

El bus entrega de forma síncrona: `publicar` llama a los suscriptores dentro de la propia llamada, como todos los que hemos construido en la unidad.

Antes de seguir, sigue la pila de llamadas a mano durante tres o cuatro niveles: `publicar` → suscriptor del reloj → `publicar` → … ¿Hay algún punto en el que alguien decida **no** volver a publicar? ¿Comprueba alguien si el tema ya era «oscuro»?

**Preguntas**

**21.** ¿Qué ocurre al ejecutarlo?

   a) Ambos aplican el tema oscuro una vez y termina
   b) Cada uno aplica el tema dos veces y termina
   c) `RangeError: Maximum call stack size exceeded`
   d) El bus ignora la segunda publicación porque el tema no ha cambiado

**22.** Si el bus usara la cola del arreglo B (entrega diferida, sin rondas anidadas), ¿se arreglaría?

   a) Sí: la cola evita las rondas anidadas y con ello el problema
   b) Sí, pero solo si hay un único suscriptor
   c) No, porque la cola descarta los mensajes repetidos
   d) No: cambia el desbordamiento de pila por un bucle infinito de mensajes

### Separar órdenes y hechos · Teoría

Dos módulos que reenvían lo que reciben forman un **ciclo de notificación**. El problema no es la entrega síncrona (por eso la cola no lo arregla), sino que el mismo topic se usa para dos cosas distintas: *pedir* un cambio y *anunciar* que ya ha ocurrido.

![Arriba, Móvil y Reloj publican y reciben por el mismo topic ajustes/tema: cada uno aplica el tema y lo reenvía, así que cada entrega vuelve a publicar y la pila crece hasta el RangeError. Abajo, la orden ajustes/cambiar-tema llega solo al dueño, ajustes, que no publica si no hay cambio; si lo hay, publica el hecho ajustes/tema-cambiado, y Reloj y Móvil solo lo aplican sin reenviarlo](/img/pde/tema-2/2b-ordenes-hechos.svg "Ciclo de notificación con un solo topic frente a órdenes y hechos separados")

<div class="cards">
  <div class="card"><h4>Orden</h4><p><code>ajustes/cambiar-tema</code>: «quiero que el tema sea oscuro». La escucha solo el dueño del dato.</p></div>
  <div class="card"><h4>Hecho</h4><p><code>ajustes/tema-cambiado</code>: «el tema ya es oscuro». Lo publica solo el dueño; los demás reaccionan y nunca lo reenvían.</p></div>
</div>

```js
// Solo el dueño escucha ÓRDENES y publica HECHOS
bus.suscribir("ajustes/cambiar-tema", (tema) => {
  if (tema === ajustes.tema) return;      // sin cambio, sin aviso
  ajustes.tema = tema;
  bus.publicar("ajustes/tema-cambiado", tema);
});

// Los demás reaccionan a hechos y nunca los reenvían
bus.suscribir("ajustes/tema-cambiado", (t) => reloj.aplicarTema(t));
bus.suscribir("ajustes/tema-cambiado", (t) => movil.aplicarTema(t));

bus.publicar("ajustes/cambiar-tema", "oscuro");
```

Tres medidas, cada una suficiente para cortar el ciclo y juntas mucho más robustas:

1. **Un único dueño** del dato `tema`: es el único que lo modifica.
2. **Órdenes y hechos en topics distintos**: quien reacciona a un hecho no publica otro hecho igual.
3. **Sin cambio, sin aviso**: si el valor no cambia, no se publica nada. (El `Observable` de la clase 3 ya lo hacía con `if (nuevo == field) return`.)

> [!ANALOGIA] El eco del grupo de WhatsApp
> Si cada vez que alguien dice «quedamos a las 8» todos responden reenviando «quedamos a las 8», el grupo no para nunca. Lo sensato es que uno *proponga*, el organizador *confirme* una vez, y los demás se limiten a leer la confirmación.

> [!EXAMEN]
> Síntoma: desbordamiento de pila o tráfico infinito. Antipatrón: ciclo de notificación. Remedio: órdenes frente a hechos; sin cambio, sin aviso.

**Preguntas**

**23.** ¿Qué medidas de la solución ayudan a cortar el ciclo?

   a) Usar un topic para órdenes y otro para hechos
   b) Pasar el bus a entrega diferida con una cola
   c) No publicar si el valor no ha cambiado
   d) Que solo el dueño del dato publique el hecho
   e) Que cada módulo reenvíe el hecho a los demás para asegurarse

**24.** ¿A qué topic se suscriben el reloj y el móvil en la solución?

### Caso: pedido cobrado tres veces · Ejercicio

Bloque 3: **antipatrones de suscripción y despacho**. Empezamos por un checkout web con un modal de pago.

```js
function abrirModalPago() {
  modal.hidden = false;
  // alta en CADA apertura
  botonConfirmar.addEventListener("click", () => {
    enviarPedido(carrito);
    modal.hidden = true;
  });
}

// cerrar no da de baja
function cancelarModal() { modal.hidden = true; }
```

El usuario duda: **abre** el modal, **cancela**, lo vuelve a **abrir**, **cancela** otra vez, lo **abre** por tercera vez y por fin pulsa **Confirmar**. Un solo clic. Nada en la interfaz delata el problema: el modal se abre y se cierra con normalidad cada vez.

> [!TRUCO]
> Recuerda de la clase 2 que `addEventListener` ignora un alta duplicada **solo si es exactamente la misma función**. Pregúntate cuántas funciones distintas hay registradas en el botón cuando llega el clic.

**Preguntas**

**25.** ¿Cuántas veces se ejecuta `enviarPedido()` con ese único clic en Confirmar? (un número)

**26.** ¿Por qué `addEventListener` no descarta los listeners repetidos?

   a) Porque solo ignora duplicados de la misma función, y cada apertura crea una lambda nueva
   b) Porque el modal está oculto cuando se registran
   c) Porque `addEventListener` nunca ignora duplicados
   d) Porque `modal.hidden = true` reinicia los listeners

### Alta única y misma referencia · Teoría

En el pedido cobrado tres veces hay dos causas: **una lambda nueva cada vez** y **sin baja simétrica** (abrir y cancelar no son un par alta/baja). Remedio: dar de alta donde se nace, no donde se repite.

```js
// Se registra UNA vez, al cargar la página, con una función con nombre
botonConfirmar.addEventListener("click", confirmar);

function confirmar() {
  enviarPedido(carrito);
  modal.hidden = true;
}

function abrirModalPago() { modal.hidden = false; }
```

La otra opción es mantener el alta en la apertura pero hacer la **baja en `cancelarModal()`**, por ejemplo registrando el listener con `{ signal }` de un `AbortController` y llamando a `abort()` al cerrar.

> [!NOTA] En Android
> El mismo fallo es un `flow.collect { }` dentro de `onResume()` sin cancelarlo: cada vuelta a primer plano añade un colector más.

### El feed que sigue cargando tras salir

Ahora el error inverso: hay baja, pero **no da de baja nada**.

```js
class FeedInfinito {
  montar() {
    window.addEventListener("scroll", this.cargarMas.bind(this));
  }
  desmontar() {
    window.removeEventListener("scroll", this.cargarMas.bind(this));
  }
  cargarMas() { if (cercaDelFinal()) this.pedirPagina(); }
}
```

`bind` crea una función nueva **en cada llamada**. La baja busca otra función, no la encuentra y calla. El usuario sale del feed a su perfil y cada scroll sigue pidiendo páginas; el `FeedInfinito` no se libera nunca.

```js
class FeedInfinito {
  cargarMas = () => {                 // campo: una sola función, fija this
    if (cercaDelFinal()) this.pedirPagina();
  };
  montar()     { window.addEventListener("scroll", this.cargarMas); }
  desmontar()  { window.removeEventListener("scroll", this.cargarMas); }
}
```

> [!IMPORTANTE] Regla
> La **misma referencia** en el alta y en la baja. En Kotlin, `sensor.removeListener { pintar(it) }` tiene el mismo fallo: esa lambda acaba de nacer y nunca estuvo en la lista.

**Preguntas**

**27.** ¿Por qué `removeEventListener("scroll", this.cargarMas.bind(this))` no quita el listener?

   a) Porque `removeEventListener` solo funciona con `window.onscroll`
   b) Porque `this` vale otra cosa dentro de `desmontar()`
   c) Porque el scroll se sigue produciendo mientras se desmonta
   d) Porque `bind` devuelve una función nueva, distinta de la que se registró

**28.** ¿Qué remedios evitan el cobro triple?

   a) Registrar el listener una sola vez al cargar la página
   b) Mantener el alta en la apertura y darlo de baja al cancelar con un `AbortController`
   c) Desactivar el botón durante 300 ms tras cada clic
   d) Registrar la misma lambda flecha escrita dentro de `abrirModalPago()`

**29.** ¿Cuál es el equivalente Android de este antipatrón que cita el material?

   a) Un `flow.collect { }` dentro de `onResume()` sin cancelarlo
   b) Llamar a `setContentView` dos veces
   c) Usar `ViewModel` en lugar de `Presenter`
   d) Registrar el `OnClickListener` en `onCreate`

### Caso: el buscador con resultados de otra búsqueda · Ejercicio

Autocompletado de una tienda. Cada vez que cambia el texto, se espera 300 ms y se piden sugerencias al servidor.

```js
input.addEventListener("input", () => {
  setTimeout(async () => {                         // espera 300 ms, nadie la cancela
    const q = encodeURIComponent(input.value);
    const res = await fetch(`/buscar?q=${q}`);
    pintarSugerencias(await res.json());
  }, 300);
});
```

El usuario escribe «mango» letra a letra: `m`, `ma`, `man`, `mang`, `mango`. Cada tecla acaba lanzando su propia petición. La de «man» resulta ser **lenta**: su respuesta llega *después* que la de «mango».

```text
teclas      m       ma      man       mang      mango
peticiones  fetch   fetch   fetch     fetch     fetch
llegadas    …  «mango» ✓  …  «man» (lenta, la última en llegar)
```

Piensa en dos cosas: cuántos temporizadores se programan, y qué respuesta es la última en llamar a `pintarSugerencias`.

**Preguntas**

**30.** ¿Qué sugerencias ve el usuario al final, con «mango» escrito?

   a) Las de «mango», porque es la última búsqueda que se pidió
   b) Ninguna: las dos respuestas se anulan
   c) Las de «man» («mandarina, manzana…»), porque es la última respuesta en llegar
   d) Las de «m», porque fue la primera

**31.** Si el usuario escribe las cinco teclas, ¿cuántos temporizadores (y por tanto peticiones) se programan como máximo con este código?

**32.** ¿Qué callbacks pendientes habría que cancelar cuando el usuario pulsa una tecla nueva?

   a) El temporizador de la tecla anterior, si aún no ha saltado
   b) La petición `fetch` anterior que sigue en vuelo
   c) El propio listener de `input`
   d) El callback de la tecla actual

### Cancelar lo pendiente: debounce y abort · Teoría

Un callback diferido que nadie cancela llega cuando ya no tiene sentido. Cada callback diferido tiene un dueño que debe poder **anularlo antes de que se ejecute**.

```js
let temporizador;
let control;

input.addEventListener("input", () => {
  clearTimeout(temporizador);                  // 1 anula el callback pendiente
  temporizador = setTimeout(async () => {
    control?.abort();                          // 2 anula la petición anterior
    control = new AbortController();
    try {
      const q = encodeURIComponent(input.value);
      const res = await fetch(`/buscar?q=${q}`, { signal: control.signal });
      pintarSugerencias(await res.json());
    } catch (e) {
      if (e.name !== "AbortError") throw e;    // 3 cancelar no es un error
    }
  }, 300);
});
```

<div class="cards">
  <div class="card"><h4>1 · Debounce</h4><p>Solo sobrevive el temporizador de la última tecla.</p></div>
  <div class="card"><h4>2 · Una petición en vuelo</h4><p>La anterior se aborta y su callback no llega a ejecutarse.</p></div>
  <div class="card"><h4>3 · AbortError se filtra</h4><p>Cancelar no es un fallo; el resto de errores se propaga.</p></div>
</div>

Con el arreglo, si el usuario escribe «mango» sin pausas largas, los temporizadores de `m`, `ma`, `man` y `mang` se anulan: 300 ms después de la última tecla sale **una** petición y se pintan los resultados de «mango».

> [!ANALOGIA] El ascensor
> El ascensor no cierra la puerta 3 segundos después de que entre el *primero*, sino 3 segundos después de que entre el *último*: cada persona nueva reinicia la cuenta. Eso es un debounce.

> [!IMPORTANTE] Regla
> **Un callback diferido es una suscripción de un solo uso: también tiene baja.** `clearTimeout`, `abort()` o `cancel()` son esa baja.

> [!EXAMEN]
> Pregunta típica: «¿qué dos callbacks pendientes hay que cancelar, y en qué momento?». El temporizador anterior, al llegar una tecla nueva; la petición anterior, al lanzar la nueva.

**Preguntas**

**33.** Ordena lo que ocurre en el código corregido desde que llega una tecla hasta que se pinta.

   Ordena: Al saltar, se aborta la petición anterior con `control?.abort()` · Se crea un `AbortController` nuevo · Se lanza `fetch` con `{ signal: control.signal }` · Se llama a `pintarSugerencias` con la respuesta · Se programa un nuevo `setTimeout` de 300 ms · `clearTimeout` anula el temporizador pendiente

**34.** ¿Por qué el `catch` filtra `AbortError` y relanza el resto?

   a) Porque `AbortError` indica que el servidor ha caído
   b) Porque `fetch` nunca lanza otros errores
   c) Para que el temporizador se reprograme solo
   d) Porque cancelar una petición a propósito no es un fallo, pero un error de red real sí debe propagarse

### Caso: la sirena que no suena · Ejercicio

Centralita domótica en C++17 sobre una Raspberry Pi. Ya recorre una foto. Tres suscriptores ante un evento de humo: una notificación push, la sirena y la apertura de puertas.

```cpp
enum class Tipo { Humo, Puerta };
struct Evento { Tipo tipo; std::string zona; };
using Handler = std::function<void(const Evento&)>;

class Centralita {
    std::vector<Handler> handlers_;
public:
    void suscribir(Handler h) { handlers_.push_back(std::move(h)); }
    void publicar(const Evento& ev) {
        auto foto = handlers_;
        for (auto& h : foto) h(ev);
    }
};

centralita.suscribir([](const Evento& ev) { enviarPush(ev); }); // sin red: lanza
centralita.suscribir([](const Evento&) { activarSirena(); });
centralita.suscribir([](const Evento&) { abrirPuertas(); });

centralita.publicar({Tipo::Humo, "cocina"});
```

Se ha ido el wifi. Hay humo en la cocina y el detector llama a `publicar`. `enviarPush` no puede conectar y lanza una excepción.

Sigue la excepción: ¿quién la captura?

**Preguntas**

**35.** ¿Qué pasa en la casa?

   a) No llega el push, pero suenan la sirena y se abren las puertas
   b) Ni sirena ni puertas: la excepción corta la ronda y sube hasta el código del detector
   c) Suena la sirena, pero las puertas no se abren
   d) La centralita reintenta el push hasta que vuelve la red y luego sigue

**36.** Si fueran tres listeners del DOM registrados con `addEventListener` y el primero lanzara, ¿qué haría el navegador?

   a) Informa de la excepción y sigue con el siguiente listener
   b) Detiene la ronda igual que el bucle de C++
   c) Elimina automáticamente el listener que ha fallado
   d) Repite el evento desde el principio

### Aislar a cada suscriptor · Teoría

Una excepción en un suscriptor interrumpe la ronda y **los que van detrás nunca se enteran**. Con humo en la cocina, que falle el push no debería impedir que suene la sirena.

```cpp
void publicar(const Evento& ev) {
    auto foto = handlers_;
    for (auto& h : foto) {
        try {
            h(ev);
        } catch (const std::exception& e) {
            registrarError(ev, e.what());       // se informa
        } catch (...) {
            registrarError(ev, "desconocida");
        }
    }
}
```

Cada suscriptor queda **aislado**: si lanza, se registra el error y el bucle sigue con el siguiente. Fíjate en que no se traga en silencio: `registrarError` deja constancia. Aislar también es decidir **quién se entera** del fallo.

![Arriba, sin aislar: el detector llama a publicar, el Push lanza porque no hay red y la excepción sube por publicar sin que nadie la capture; la Sirena y las Puertas no se enteran. Abajo, con try/catch por suscriptor: el error del Push queda registrado y la ronda sigue, así que la Sirena suena y las Puertas se abren](/img/pde/tema-2/2b-excepcion-aislar.svg "La sirena que no suena: excepción sin aislar frente a try/catch por suscriptor")

### Cada plataforma decide

| Plataforma | Si un listener lanza… |
| --- | --- |
| DOM (`addEventListener`) | Informa de la excepción y sigue con el siguiente |
| Node `EventEmitter` | Se detiene |
| Bucle propio en Kotlin o C++ | Se detiene (salvo que lo aisléis como arriba) |

> [!IMPORTANTE]
> Hay que saber cuál es el caso de vuestra plataforma. Es la pregunta 4 de la letra pequeña: «Si un suscriptor lanza, ¿se sigue avisando a los demás?».

### El primo de la excepción: el handler lento

El despacho es síncrono: si un suscriptor **tarda** 3 segundos (por ejemplo, hace una petición de red en su turno), retrasa a todos los que van detrás y, si está en el hilo de la interfaz, la congela. El remedio es **delegar y retornar**: el handler encarga el trabajo largo y devuelve el control enseguida. Lo estudiaremos a fondo en la Unidad 3.

> [!EXAMEN]
> Síntoma: «tras un fallo, el resto no reacciona». Antipatrón: excepción sin aislar. Remedio: `try/catch` por suscriptor, registrando el error.

**Preguntas**

**37.** ¿En qué casos se **detiene** la ronda si un suscriptor lanza (sin aislar)?

   a) Listeners del DOM con `addEventListener`
   b) Node `EventEmitter`
   c) Un `for` escrito a mano en Kotlin
   d) Un `for` escrito a mano en C++

**38.** ¿Dónde se coloca el `try/catch` para aislar a cada suscriptor?

   a) Alrededor de toda la llamada a `publicar` en el detector
   b) Dentro de `suscribir`
   c) En el constructor de `Centralita`
   d) Dentro del `for`, alrededor de cada `h(ev)`

### Caso: el termostato que se reinicia sin motivo · Ejercicio

Termostato con pantalla (ESP32, Arduino, C++). Al entrar en el menú de ajustes se crea una pantalla que se suscribe al sensor; al pulsar «Atrás» se destruye.

```cpp
class SensorTemp {
    std::vector<std::function<void(float)>> oyentes_;
public:
    void suscribir(std::function<void(float)> f) { oyentes_.push_back(std::move(f)); }
    void publicar(float t) { for (auto& f : oyentes_) f(t); }
};

class PantallaAjustes {
    float ultima_ = 0;
public:
    explicit PantallaAjustes(SensorTemp& s) {
        s.suscribir([this](float t) { ultima_ = t; pintar(); }); // captura this
    }
    void pintar();
};

void menuAjustes(SensorTemp& sensor) {
    auto* p = new PantallaAjustes(sensor);
    esperarBotonAtras();
    delete p;                                   // la lambda sigue en oyentes_
}

// 5 s después: sensor.publicar(22.5) → ¿?
```

En Kotlin o JavaScript ya conocemos este olvido (clase 2): el recolector de basura mantiene viva la pantalla, con su fuga de memoria y sus callbacks fantasma. Pero en C++ **no hay recolector**: `delete p` libera la memoria aunque alguien siga apuntando a ella.

**Preguntas**

**39.** ¿Qué ocurre cuando el sensor publica 22,5 °C cinco segundos después?

   a) Nada: la lambda detecta que la pantalla ya no existe
   b) El compilador lo impide: no deja capturar `this` en una lambda
   c) La pantalla se vuelve a crear automáticamente
   d) La lambda escribe en memoria liberada a través de un puntero colgante: basura en el display, un reinicio… o nada durante semanas

**40.** ¿Qué mecanismo de C++ se ejecuta **siempre** cuando un objeto deja de existir?

### RAII: la suscripción se da de baja sola · Teoría

Sin recolector de basura, el listener olvidado no provoca una fuga: provoca un **puntero colgante**. La solución en C++ es **RAII**: si la suscripción es un *miembro* del suscriptor, muere con él, y su destructor hace la baja.

```cpp
class SensorTemp {
    std::map<int, std::function<void(float)>> oyentes_;
    int siguiente_ = 0;
public:
    class Suscripcion {                                   // el objeto «baja»
        SensorTemp* sensor_; int id_;
    public:
        Suscripcion(SensorTemp* s, int id) : sensor_(s), id_(id) {}
        ~Suscripcion() { sensor_->oyentes_.erase(id_); }  // baja
        Suscripcion(const Suscripcion&) = delete;
        Suscripcion& operator=(const Suscripcion&) = delete;
    };

    std::unique_ptr<Suscripcion> suscribir(std::function<void(float)> f) {
        int id = siguiente_++;
        oyentes_[id] = std::move(f);
        return std::make_unique<Suscripcion>(this, id);
    }

    void publicar(float t) {
        auto foto = oyentes_;
        for (auto& [id, f] : foto) f(t);
    }
};

class PantallaAjustes {
    std::unique_ptr<SensorTemp::Suscripcion> sub_;   // miembro
public:
    explicit PantallaAjustes(SensorTemp& s)
        : sub_(s.suscribir([this](float t) { pintar(t); })) {}
    void pintar(float t);
}; // delete pantalla → ~sub_ → baja automática
```

<div class="cards">
  <div class="card"><h4>1 · La suscripción es un objeto</h4><p>Es la suscripción cancelable de la clase 2, ahora con forma de clase.</p></div>
  <div class="card"><h4>2 · Destructor = baja</h4><p>Nadie tiene que acordarse: la llama el compilador.</p></div>
  <div class="card"><h4>3 · Mismo tiempo de vida</h4><p>Miembro de la pantalla y no copiable: muere con ella.</p></div>
</div>

> [!NOTA] El mismo patrón en otras plataformas
> `DisposableEffect` en Compose, `AbortController` en el DOM o Qt al destruir un `QObject`. **La baja la hace el ciclo de vida, no la disciplina.**

| Sin baja… | Consecuencia |
| --- | --- |
| Kotlin / JavaScript | El recolector mantiene viva la pantalla: fuga de memoria y callbacks fantasma |
| C++ | La pantalla ya no existe: basura en el display, reinicio… o nada durante semanas |

**Preguntas**

**41.** ¿Por qué `Suscripcion` borra su constructor de copia y su asignación por copia?

   a) Para que no haya dos objetos cuyo destructor intente dar de baja la misma suscripción
   b) Para que ocupe menos memoria en el ESP32
   c) Porque `std::map` no admite objetos copiables
   d) Para que la lambda pueda capturar `this`

**42.** ¿Qué mecanismos cita el material como el mismo patrón (la baja la hace el ciclo de vida)?

   a) `DisposableEffect` en Compose
   b) `AbortController` en el DOM
   c) Qt al destruir un `QObject`
   d) `setTimeout` en JavaScript
   e) `println` en Kotlin

### Ficha de diagnóstico: del síntoma al antipatrón · Ejercicio

En producción nunca te dicen «hay una reentrancia en la línea 12». Te dicen «la pantalla pone 12 y hay 10». La **ficha de diagnóstico** es un mapa que va del **síntoma que se observa** al **antipatrón** que lo causa y a su **remedio**. La usaremos tal cual en la clase 5.

Intenta responder las preguntas sin abrir la ficha; después compruébala completa.

<details><summary>Ver la ficha completa (11 filas)</summary>
<table>
<thead><tr><th>#</th><th>Síntoma que se observa</th><th>Antipatrón</th><th>Remedio</th></tr></thead>
<tbody>
<tr><td>1</td><td>El resultado depende del orden de carga de los módulos</td><td>Suscriptores que dependen unos de otros</td><td>Estado derivado en el sujeto</td></tr>
<tr><td>2</td><td>Un suscriptor nunca se entera, sin ningún error</td><td>Baja durante el recorrido de la lista viva</td><td>Foto de la lista</td></tr>
<tr><td>3</td><td>Un suscriptor dado de baja reacciona una última vez</td><td>Foto sin comprobar si sigue activo</td><td>Bandera <code>activa</code></td></tr>
<tr><td>4</td><td>La vista muestra un valor distinto del modelo</td><td>Reentrancia: un observador corrige al sujeto</td><td>Regla en el sujeto / cola</td></tr>
<tr><td>5</td><td>Desbordamiento de pila o tráfico infinito</td><td>Ciclo de notificación</td><td>Órdenes vs hechos; sin cambio, sin aviso</td></tr>
<tr><td>6</td><td>La acción se ejecuta 2, 3… N veces</td><td>Alta en un método que se repite</td><td>Alta única o baja simétrica</td></tr>
<tr><td>7</td><td>Pantalla cerrada que reacciona; memoria que crece</td><td>Baja con otra referencia</td><td>Misma referencia / suscripción</td></tr>
<tr><td>8</td><td>Aparecen resultados de una búsqueda anterior</td><td>Callback diferido sin cancelar</td><td><code>clearTimeout</code>, <code>abort</code>, <code>cancel</code></td></tr>
<tr><td>9</td><td>Tras un fallo, el resto no reacciona</td><td>Excepción sin aislar</td><td><code>try/catch</code> por suscriptor</td></tr>
<tr><td>10</td><td>Reinicios aleatorios en C/C++</td><td>Puntero colgante en el callback</td><td>RAII: suscripción como miembro</td></tr>
<tr><td>11</td><td>La interfaz se congela mientras un handler trabaja</td><td>Handler lento en la ronda</td><td>Delegar y retornar (U3 / U6)</td></tr>
</tbody>
</table>
</details>

> [!EXAMEN]
> Un ejercicio de diagnóstico típico da el síntoma (o la salida de un programa) y pide tres cosas: el antipatrón, el remedio y la pregunta del contrato que nadie se hizo.

**Preguntas**

**43.** Síntoma: «cada vez que vuelvo a la pantalla de chat, los mensajes nuevos suenan una vez más: 1, 2, 3 pitidos…». ¿Antipatrón?

   a) Ciclo de notificación
   b) Callback diferido sin cancelar
   c) Alta en un método que se repite (sin baja simétrica)
   d) Puntero colgante en el callback

**44.** Síntoma: «el suscriptor de estadísticas a veces no registra nada, y no hay ningún error en el log». Justo antes, otro suscriptor se da de baja durante su turno. ¿Remedio?

   a) `try/catch` por suscriptor
   b) Recorrer una foto de la lista
   c) Separar órdenes y hechos
   d) Debounce con `clearTimeout`

**45.** Síntoma: «al recibir un mensaje, la interfaz se queda congelada un par de segundos». Un suscriptor sube una copia al servidor en su turno. ¿Remedio?

   a) Delegar el trabajo lento y retornar enseguida
   b) Recorrer una foto de la lista
   c) Usar RAII para la suscripción
   d) Publicar el mensaje dos veces

### GoF Observer, MVP y MVVM: quién conoce a quién · Teoría

Bloque 4. Los tres modelos son **la misma idea colocada en sitios distintos**. Para compararlos, sigue la flecha que llega a la vista: indica quién conoce a quién.

<div class="cards">
  <div class="card"><h4>GoF Observer</h4><p>El sujeto conoce una lista de «observadores», no sus tipos. Les llama con <code>update(t)</code>.</p></div>
  <div class="card"><h4>MVP</h4><p>El Presenter conoce la vista (a través de una interfaz) y le <b>ordena</b> qué pintar: <code>mostrar(n)</code>.</p></div>
  <div class="card"><h4>MVVM</h4><p>El ViewModel <b>no</b> conoce la vista: la vista se <b>suscribe</b> a su estado observable.</p></div>
</div>

![Tres filas. GoF Observer: el sujeto, que conoce una lista, llama a update(t) en N observadores de cualquier tipo. MVP: el Presenter guarda la vista y le llama con mostrar(n); la vista, con la interfaz ContadorView, le avisa con onPulsar(). MVVM: la vista avisa al ViewModel con onPulsar() y observa su estado con una suscripción; el ViewModel no conoce la vista y ninguna flecha llega a ella](/img/pde/tema-2/2b-quien-conoce-a-quien.svg "Quién conoce a quién: sigue la flecha que llega a la vista")

### El mismo contador en los dos

```kotlin
// MVP · el Presenter ordena pintar
interface ContadorView {                       // contrato de la vista
    fun mostrarPulsaciones(n: Int)
    fun habilitarBoton(activo: Boolean)
}

class ContadorPresenter(private var vista: ContadorView?) {
    private var pulsaciones = 0
    private var activo = true
    fun onPulsar() {
        if (!activo) return
        pulsaciones++
        vista?.mostrarPulsaciones(pulsaciones)  // ordena
    }
    fun onCambiarActivo(v: Boolean) {
        activo = v; vista?.habilitarBoton(v)
    }
}
// Activity : ContadorView
// boton.setOnClickListener { presenter.onPulsar() }
// override fun mostrarPulsaciones(n: Int) { texto.text = "…$n" }
```

```kotlin
// MVVM · la vista observa el estado (clase 3)
class ContadorViewModel {                      // no conoce ninguna vista
    val pulsaciones = Observable(0)
    val activo = Observable(true)
    fun onPulsar() {
        if (activo.valor) pulsaciones.valor++  // solo estado
    }
    fun onCambiarActivo(v: Boolean) { activo.valor = v }
}
// Activity
// boton.setOnClickListener { vm.onPulsar() }
// vm.pulsaciones.observar { texto.text = "Pulsaciones: $it" }
// vm.activo.observar { boton.isEnabled = it }
```

> [!IMPORTANTE] Fíjate
> El listener del botón es **idéntico** en los dos: solo avisa. Lo que cambia es la vuelta: el Presenter hace **una llamada** a la vista; el ViewModel cambia **un estado** que la vista observa. De esa diferencia salen los errores típicos de cada uno.

**Preguntas**

**46.** ¿En qué modelo el componente de lógica **no** tiene ninguna referencia a la vista?

   a) MVP
   b) MVVM
   c) En los dos
   d) En ninguno

**47.** ¿Qué parte del código es igual en la versión MVP y en la MVVM?

   a) El listener del botón: solo avisa a la lógica
   b) Cómo llega el nuevo valor a la pantalla
   c) La interfaz `ContadorView`
   d) El uso de `Observable`

### Comparativa y regla de MVVM · Teoría

Qué aporta cada modelo y dónde suele fallar:

|  | GoF Observer | MVP | MVVM |
| --- | --- | --- | --- |
| Quién conoce a quién | El sujeto, una lista de observadores | Presenter ↔ Vista (por interfaz) | La vista conoce el ViewModel; el ViewModel no conoce la vista |
| Cómo llega el cambio | `notify()` → `update()` en cada uno | Llamada directa `vista.mostrar…()` | La vista observa el estado |
| Número de vistas | N | Normalmente 1 | N |
| Cómo se prueba | Sujeto + observador falso | Presenter + vista falsa | ViewModel solo; se lee el estado |
| Rotación / ciclo de vida | No lo contempla | A mano: adjuntar / soltar | Lo resuelve el framework (ViewModel, LiveData, `repeatOnLifecycle`) |
| Error típico | Observador que modifica al sujeto (reentrancia) | Presenter que llama a una vista muerta | Evento puntual tratado como estado, o vista guardada en el ViewModel |
| Dónde lo veis | `addListener`, `EventTarget`, `Sensor` | Android clásico, WinForms | Android Jetpack, WPF, Vue, SwiftUI |

> [!IMPORTANTE] Regla
> En MVVM **la flecha nunca apunta del ViewModel a la vista**. Si un ViewModel guarda una `Activity`, una `View` o un `Context` de pantalla, se ha convertido en un MVP mal hecho, con fuga de memoria incluida.

> [!ANALOGIA] El tablón y el encargado
> En MVP, el Presenter es un encargado que llama por teléfono a cada empleado para decirle qué hacer: si el empleado se ha ido, la llamada se pierde (o peor). En MVVM, el ViewModel escribe en un tablón y cada empleado que llega lo lee: no necesita saber quién está.

> [!EXAMEN]
> Una pregunta clásica: «¿qué diferencia hay entre MVP y MVVM?». La respuesta corta: en MVP el Presenter **llama** a la vista; en MVVM la vista **se suscribe** al estado del ViewModel, que no la conoce. De ahí salen la prueba, el número de vistas y los errores típicos.

**Preguntas**

**48.** ¿Cómo se prueba la lógica en MVVM según la tabla?

   a) Con una vista falsa que registra las llamadas
   b) Arrancando la Activity en un emulador
   c) Con un observador falso y un sujeto real
   d) Con el ViewModel solo, leyendo su estado

**49.** ¿Cuáles de estas cosas **no** debería guardar nunca un ViewModel?

   a) Una referencia a la `Activity`
   b) Un `StateFlow` con el estado de la pantalla
   c) Una `View` concreta, como un `TextView`
   d) Un `Context` de pantalla
   e) Un repositorio de datos

### Caso MVP: el perfil que llega a una pantalla muerta · Ejercicio

Android con MVP. El Presenter recibe la vista en el constructor y la guarda.

```kotlin
class PerfilPresenter(
    private val vista: PerfilView,      // referencia fija
    private val repo: Repositorio
) {
    fun cargar() {
        repo.cargarPerfil { perfil ->    // 3 s de red
            vista.mostrarPerfil(perfil)  // ¿sigue viva?
        }
    }
}
```

Escena: el usuario abre su perfil, se llama a `cargar()` y la petición tarda 3 segundos. **A mitad de la espera gira el móvil.** En Android, girar destruye la `Activity` y crea una nueva (lo vimos al hablar del ciclo de vida).

Piensa en dos cosas:

- Guardar la vista en el Presenter, ¿a qué equivale en el vocabulario de la unidad? ¿Hay su pareja?
- Cuando llega la respuesta, ¿a qué `Activity` apunta `vista`?

**Preguntas**

**50.** ¿Qué ve el usuario cuando llega la respuesta?

   a) El perfil en la nueva Activity, sin problemas
   b) Un error de compilación por usar `vista` en una lambda
   c) La nueva Activity vacía: el perfil se pinta en la Activity muerta, que además no se puede liberar (fuga)
   d) El perfil aparece dos veces, una en cada Activity

**51.** En el lenguaje de la unidad, guardar la vista en el Presenter es…

   a) …un alta implícita sin su baja
   b) …una baja anticipada
   c) …un callback síncrono
   d) …una foto de la lista

### Adjuntar y soltar la vista · Teoría

El Presenter guarda la vista, que es **un alta implícita**. Sin baja, el callback de red la encuentra muerta. La solución es dar al Presenter un alta y una baja explícitas, ligadas al ciclo de vida de la Activity:

```kotlin
class PerfilPresenter(private val repo: Repositorio) {
    private var vista: PerfilView? = null
    private var ultimo: Perfil? = null

    fun adjuntar(v: PerfilView) {
        vista = v
        ultimo?.let { v.mostrarPerfil(it) }   // repinta al volver
    }

    fun soltar() { vista = null }

    fun cargar() = repo.cargarPerfil { p ->
        ultimo = p
        vista?.mostrarPerfil(p)
    }
}
// Activity: onStart { adjuntar(this) }  onStop { soltar() }
```

Tres cambios:

1. **`vista` es anulable**: puede no haber nadie escuchando, y `vista?.` no llama si es `null`.
2. **Adjuntar y soltar** son el alta y la baja, colocadas en `onStart` y `onStop`.
3. **Estado guardado (`ultimo`)**: si la respuesta llega cuando no hay vista, se guarda; al adjuntar la nueva, se repinta con el último valor.

Para que el arreglo funcione tras un giro, el Presenter tiene que sobrevivir a la recreación de la Activity (si se crea uno nuevo con cada Activity, se pierde `ultimo`).

> [!NOTA] MVP acaba reinventando MVVM
> Un MVP bien hecho acaba reinventando la mitad de MVVM: **estado guardado**, **alta y baja ligadas al ciclo de vida** y **repintado al suscribirse**. Por eso Android pasó a MVVM, donde el framework ya da todo eso hecho.

**Preguntas**

**52.** ¿En qué métodos del ciclo de vida coloca el material `adjuntar` y `soltar`?

   a) `onCreate` y `onDestroy`
   b) `onResume` y `onCreate`
   c) En el constructor del Presenter
   d) `onStart` y `onStop`

**53.** ¿Para qué sirve `ultimo` en el Presenter corregido?

   a) Para no volver a llamar a la red si el perfil ya se cargó una vez
   b) Para saber cuál fue la última vista adjuntada
   c) Para cancelar la petición en curso
   d) Para repintar la nueva vista con el último perfil cuando se adjunta

### Caso MVVM: el Toast que vuelve al girar · Ejercicio

Ahora MVVM. Al confirmar un pedido, el ViewModel quiere que la pantalla muestre «Pedido enviado ✔».

```kotlin
class PedidoViewModel : ViewModel() {
    val mensaje = MutableLiveData<String>()   // un aviso como estado
    fun onConfirmar() {
        // … enviar el pedido
        mensaje.value = "Pedido enviado ✔"
    }
}

// PedidoActivity.onCreate
vm.mensaje.observe(this) { texto ->
    Toast.makeText(this, texto, Toast.LENGTH_SHORT).show()
}
```

Recuerda dos cosas de la clase 3:

- `LiveData` **guarda el último valor**. Justo lo que queríamos para `pulsaciones`: al suscribirse, la vista recibe el valor actual, como `o(valor)` en `observar()`.
- El `ViewModel` **sobrevive** al giro; la Activity no. La nueva Activity hace un `observe()` nuevo, es decir, **una suscripción nueva**.

Escena: el usuario confirma, ve el Toast y después gira el móvil. Y lo vuelve a girar.

**Preguntas**

**54.** ¿Qué ocurre cada vez que el usuario gira el móvil?

   a) Nada: el Toast solo se muestra al confirmar
   b) El Toast «Pedido enviado ✔» vuelve a aparecer
   c) La app falla porque el ViewModel apunta a la Activity vieja
   d) El pedido se envía otra vez

**55.** Si en lugar de un Toast el observador hiciera `navegar(PantallaConfirmacion)`, ¿qué notaría el usuario al girar o al volver con «Atrás»?

   a) Que la app lo vuelve a llevar a la confirmación cada vez que la pantalla del pedido se suscribe de nuevo
   b) Nada, la navegación solo ocurre una vez
   c) Que el botón «Atrás» deja de funcionar por completo
   d) Que la confirmación aparece vacía

### Estado frente a evento, con Flow · Teoría

Repintar con el valor actual al suscribirse es **correcto para un estado e incorrecto para un aviso**. Lo que es un estado se repinta siempre; lo que es un aviso se consume una sola vez.

```kotlin
data class PedidoUi(val enviando: Boolean = false, val enviado: Boolean = false)

class PedidoViewModel(private val repo: Repositorio) : ViewModel() {
    private val _estado = MutableStateFlow(PedidoUi())          // ESTADO
    val estado: StateFlow<PedidoUi> = _estado

    private val _avisos = Channel<String>(Channel.BUFFERED)     // EVENTO
    val avisos = _avisos.receiveAsFlow()

    fun onConfirmar() = viewModelScope.launch {
        _estado.update { it.copy(enviando = true) }
        repo.enviar()
        _estado.update { it.copy(enviando = false, enviado = true) }
        _avisos.send("Pedido enviado ✔")
    }
}

// PedidoActivity.onCreate
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {   // alta/baja solas
        launch { vm.estado.collect { pintar(it) } }  // se repinta al girar
        launch { vm.avisos.collect { toast(it) } }   // se muestra una vez
    }
}
```

- **`StateFlow`** guarda el valor actual y lo entrega a cada suscriptor nuevo: estado.
- **`Channel` + `receiveAsFlow()`**: cada aviso lo consume un único colector una sola vez; al girar no se repite.
- **`repeatOnLifecycle(STARTED)`** hace el alta al llegar a STARTED y la baja al salir: el adjuntar/soltar del MVP, gratis.

### El modelo de la Unidad 2 en Android

| Unidad 2 | Android |
| --- | --- |
| `observar()` / `suscribir()` | `collect { }` / `observe()` |
| Valor actual al suscribirse | `StateFlow`, `LiveData` |
| Aviso que no se repite | `Channel` + `receiveAsFlow()` |
| Toda alta tiene su baja | `repeatOnLifecycle(STARTED)` |
| `OnClickListener` que solo avisa | `vm.onConfirmar()` |

> [!IMPORTANTE] Regla
> Antes de exponer algo desde un ViewModel, pregúntate: **¿es algo que ES o algo que HA PASADO?** Lo primero es estado; lo segundo, evento. «El pedido está enviado» es estado; «el pedido se acaba de enviar» es evento.

**Preguntas**

**56.** ¿Cuáles de estos son **estado** (algo que ES) y deberían repintarse al girar?

   a) El número de pulsaciones del contador
   b) «Mostrar el Toast: pedido enviado»
   c) Que el pedido está enviado (`enviado = true`)
   d) «Navegar a la pantalla de confirmación»
   e) Que el botón está deshabilitado mientras se envía

**57.** ¿Qué tipo de Kotlin usa el ViewModel corregido para los avisos de un solo uso (antes de convertirlo con `receiveAsFlow`)?

### Caso integrador: encuentra los cuatro fallos · Ejercicio

Pantalla de seguimiento de una app de reparto. Compila, funciona en la demo… y falla en producción.

```kotlin
class SeguimientoActivity : AppCompatActivity() {
    override fun onResume() {
        super.onResume()
        Pedidos.suscribir { estado ->
            textoEstado.text = estado
            if (estado == "entregado") {
                Pedidos.publicar("pendiente-valoracion")
                Pedidos.desuscribir { }
            }
            Analitica.enviar(estado) // petición HTTP; lanza si no hay red
        }
    }
}
```

Pistas: hay **cuatro** fallos, uno de cada familia:

- uno de **ciclo de vida**,
- uno de **referencia**,
- uno de **reentrancia**,
- uno de **aislamiento o lentitud**.

Para cada fallo: línea, síntoma que vería el usuario y remedio. Contesta la pregunta y después abre la solución.

<details><summary>Ver la solución</summary>
<table>
<thead><tr><th>#</th><th>Fallo</th><th>Síntoma</th><th>Regla</th></tr></thead>
<tbody>
<tr><td>1</td><td>Alta en <code>onResume()</code> sin baja</td><td>Cada vuelta a primer plano añade un suscriptor: N repintados, fuga</td><td>Alta y baja simétricas</td></tr>
<tr><td>2</td><td>Publicar en el mismo sujeto en su ronda</td><td>Los demás reciben <code>pendiente-valoracion</code> antes que <code>entregado</code></td><td>Reentrancia</td></tr>
<tr><td>3</td><td><code>desuscribir { }</code> con lambda nueva</td><td>No da de baja nada</td><td>Misma referencia</td></tr>
<tr><td>4</td><td>Red en el handler, hilo principal, sin aislar</td><td>UI congelada; sin red, la excepción corta la ronda</td><td>Aislar y delegar</td></tr>
</tbody>
</table>
<pre><code>class SeguimientoActivity : AppCompatActivity() {
    private var baja: (() -&gt; Unit)? = null
    override fun onStart() {
        super.onStart()
        baja = Pedidos.suscribir { estado -&gt;      // 1 alta con baja
            textoEstado.text = estado
            if (estado == "entregado")
                mostrarValoracion()               // 2 sin republicar
        }
    }
    override fun onStop() {
        baja?.invoke(); baja = null               // 3 baja simétrica
        super.onStop()
    }
}
// 4 Analítica: fuera de la pantalla y en segundo plano
Pedidos.suscribir { estado -&gt;
    appScope.launch(Dispatchers.IO) {
        runCatching { Analitica.enviar(estado) }
    }
}</code></pre>
</details>

**Preguntas**

**58.** Marca los **cuatro** fallos reales de `SeguimientoActivity`.

   a) Suscribirse en `onResume()` sin dar de baja en ningún sitio
   b) Asignar `textoEstado.text` dentro del suscriptor
   c) Publicar `pendiente-valoracion` desde dentro de la ronda de `Pedidos`
   d) `desuscribir { }` con una lambda recién creada
   e) Comparar `estado` con `==` en lugar de `equals`
   f) Hacer una petición HTTP que puede lanzar dentro del handler, en el hilo principal

**59.** En la versión corregida, ¿qué consiguen `appScope.launch(Dispatchers.IO)` y `runCatching` alrededor de `Analitica.enviar`?

   a) Que la analítica se envíe antes que el repintado
   b) Que la analítica solo se envíe cuando la pantalla está visible
   c) Que la petición se repita hasta tener red
   d) Delegar el trabajo lento fuera del hilo principal y aislar su excepción para que no corte la ronda

### Repaso: lista de comprobación ampliada y puente a la U3 · Repaso

Antes de dar por buena cualquier suscripción, pasa esta lista. Las seis primeras preguntas vienen de la clase 2; las cuatro nuevas son el **contrato de entrega** que hemos visto hoy.

| # | Pregunta | Qué buscas |
| --- | --- | --- |
| 1 | ¿Cuál es el contrato del callback? | Qué recibe, qué devuelve, cuándo se llama |
| 2 | ¿Síncrono o diferido? | ¿Corre dentro de la llamada o más tarde (y en qué hilo)? |
| 3 | ¿Cuántos suscriptores? | Uno o N |
| 4 | ¿Quién da de baja? | Con la misma referencia que el alta |
| 5 | ¿Cómo se asocia evento → handler? | Lista, mapa, topic… |
| 6 | ¿De dónde saca el handler sus datos? | Del evento, no del emisor |
| 7 | ¿Qué pasa con las altas y bajas durante una ronda? | Foto pura o foto + activa |
| 8 | ¿Puede un suscriptor publicar? | ¿Se entrega ya o se encola? ¿Puede formar un ciclo? |
| 9 | ¿Qué pasa si un suscriptor falla o tarda? | ¿Se aísla? ¿Quién se entera? |
| 10 | ¿Es un estado o un evento? | ¿Debe recibirlo quien llega tarde? |

> [!RESUMEN]
> - Las respuestas a orden, altas/bajas, reentrancia y fallos **son la API**, aunque no estén en la firma.
> - Recorre siempre **una foto**; decide si la baja cuenta desde ya (bandera `activa`).
> - Un observador no corrige al sujeto: **regla en el sujeto**, o cola si no hay más remedio.
> - **Órdenes y hechos** en topics distintos; sin cambio, sin aviso.
> - Toda alta tiene su baja, **con la misma referencia**; un callback diferido también.
> - **Aísla** a cada suscriptor; en C++, la baja en un **destructor** (RAII).
> - MVP **ordena** a la vista; MVVM **expone estado** y la vista se suscribe. Estado ≠ evento.

### Puente a la Unidad 3

La cola `pendientes` + `while` del arreglo B es **un bucle de eventos en miniatura**: productor → cola → dispatcher → handler, y cada ronda termina antes de la siguiente (*run-to-completion*). En la Unidad 3 se construye completo, con prioridades, y se estudia su peor enemigo: **el handler lento** que congela la pantalla.

**Preguntas**

**60.** Un suscriptor del `Observable` corrige el valor durante su turno y la vista acaba mostrando un valor viejo. ¿Qué pregunta de la lista nadie se hizo?

   a) 3 · ¿Cuántos suscriptores?
   b) 10 · ¿Es un estado o un evento?
   c) 8 · ¿Puede un suscriptor publicar? ¿Se entrega ya o se encola?
   d) 5 · ¿Cómo se asocia evento → handler?

**61.** Completa la regla: «Un callback diferido es una suscripción de un solo uso: también tiene …»

**62.** ¿Qué elemento de esta clase es «un bucle de eventos en miniatura» que se desarrollará en la Unidad 3?

   a) La foto `toList()`
   b) El `try/catch` por suscriptor
   c) La interfaz `ContadorView` del MVP
   d) La cola `pendientes` con su `while` (arreglo B)

### Solucionario

1. c) Cuando `publicar()` retorna  
   La ronda empieza al entrar en `publicar()` y termina cuando `publicar()` retorna. Si un suscriptor publica durante su turno, se abre una **ronda anidada** dentro de la primera.
2. a) ¿Se garantiza el orden de alta?; c) ¿Las altas y bajas afectan a la ronda en curso o a la siguiente?; d) ¿Se puede publicar desde un suscriptor?; f) Si un suscriptor lanza, ¿se sigue avisando a los demás?  
   Orden, altas/bajas durante la ronda, reentrancia y fallos. Ninguna aparece en la firma, pero todas forman parte del contrato de la API.
3. b) Nada: el total ya es 55 € y no hay banner  
   El banner va primero y lee `resumen.total` cuando aún vale 45 €. Después el resumen lo sube a 55 €, pero el banner ya ha decidido. No hay ningún error: simplemente no sale nada.
4. c) Aparece el banner: el fallo desaparece… hasta que otro cambio de orden lo traiga de vuelta  
   El resultado depende del orden de carga de los módulos. Reordenar «arregla» este caso, pero el fallo puede reaparecer en otro sitio: la dependencia sigue escondida.
5. d) Que el carrito actualice su total y publique `carrito/cambiado` con el total ya calculado  
   El estado derivado (el total) vive en el sujeto. El evento ya lleva el dato correcto, así que los suscriptores no dependen unos de otros ni del orden de carga. Las otras opciones solo esconden la dependencia.
6. `total`  
   El banner desestructura `{ total }` del propio evento: no consulta a `resumen` ni a nadie más.
7. a) `Mapa: entregado`; c) `Historial: entregado`  
   Salen `Mapa: entregado` e `Historial: entregado`. Nadie pide la valoración y **no hay ningún error**: JavaScript no protesta si modificas el array mientras lo recorres.
8. c) Valoración en la posición 0; el bucle pasa a la 1  
   Al quitar el elemento 0, Valoración baja a la posición 0 e Historial a la 1. El bucle ya había procesado el índice 0, así que pasa al 1 y llama a Historial: Valoración se queda sin turno.
9. `ConcurrentModificationException`  
   `ConcurrentModificationException`: el iterador detecta que la lista ha cambiado mientras se recorría. No es concurrencia de hilos: basta con un solo hilo que modifica la lista dentro del bucle.
10. a) Porque un fallo que se ve siempre es mejor que un suscriptor que desaparece en silencio  
   La excepción te obliga a arreglarlo el primer día. En JavaScript el programa sigue funcionando y la valoración simplemente no se pide: puede pasar meses sin que nadie lo note.
11. d) «Ha llegado a su destino» y después «En 200 m, gire a la derecha»  
   La foto se hizo al empezar la ronda y `voz` estaba en ella. Aunque se cancele durante el turno de `controlRuta`, el bucle llega a su posición en la foto y la llama: el conductor oye una instrucción después de haber llegado.
12. a) Porque el bucle recorre una copia (`toList()`), no la lista que se modifica  
   La foto protege al iterador: la lista real cambia, pero el bucle recorre otra. El precio es que la foto puede contener a alguien que ya se ha dado de baja.
13. b) …no recibe el evento en curso  
   El DOM se comporta como «foto + comprobar activa»: la baja cuenta desde ya. Node `EventEmitter` es foto pura: el retirado recibe el evento en curso.
14. c) …espera a la siguiente ronda  
   La foto se hizo antes del alta, así que el nuevo suscriptor no está en ella: su primer aviso será el de la siguiente ronda.
15. `activa`  
   `activa`: `cancelar` la pone a `false` y el bucle salta las suscripciones inactivas aunque sigan en la foto.
16. 1. [Carrito] cantidad: 10 → 2. [Carrito] cantidad: 12 → 3. valor real: 10  
   Ronda 1 (valor 12): `limitar(12)` pone el valor a 10 y abre la ronda 2, que pinta «10». Al volver, la ronda 1 sigue con su `nuevo = 12` y pinta «12». El valor real es 10, pero lo último que se ha pintado es 12.
17. a) Que lo último que pinta la vista coincida con el valor real del modelo  
   La vista se queda con «12» cuando el modelo vale 10. El síntoma típico de la reentrancia: la vista muestra un valor distinto del modelo.
18. d) Porque con A el valor inválido nunca se publica; con B la vista llega a mostrar el 12 un instante  
   B evita que las rondas se aniden y deja la vista con el valor correcto al final, pero el 12 llega a pintarse. A elimina el problema de raíz: el sujeto valida y los observadores solo pintan.
19. `run-to-completion`  
   **Run-to-completion**: cada evento se procesa entero antes del siguiente. Es la base del bucle de eventos de la Unidad 3.
20. c) Guarda el 10 en `pendientes` y retorna, porque `notificando` es `true`  
   La reentrada solo encola. El `while` de la ronda que ya está en marcha recogerá el 10 cuando termine de repartir el 12.
21. c) `RangeError: Maximum call stack size exceeded`  
   Es un **ciclo de notificación**: cada publicación abre una ronda anidada que vuelve a publicar. Nadie corta la cadena y la pila de llamadas se desborda.
22. d) No: cambia el desbordamiento de pila por un bucle infinito de mensajes  
   La cola impide que la pila crezca, pero cada entrega vuelve a encolar otro aviso: la cola nunca se vacía. Si los módulos hablaran por MQTT entre dos dispositivos, sería tráfico sin fin y batería agotada.
23. a) Usar un topic para órdenes y otro para hechos; c) No publicar si el valor no ha cambiado; d) Que solo el dueño del dato publique el hecho  
   Separar órdenes y hechos, un único dueño que publica el hecho y «sin cambio, sin aviso». La cola solo convierte el desbordamiento en un bucle infinito de mensajes.
24. `ajustes/tema-cambiado`  
   `ajustes/tema-cambiado`: es un hecho. Reaccionan aplicando el tema y no publican nada.
25. `3`  
   Tres. Cada apertura añade un listener nuevo y cancelar no quita ninguno. Al pulsar Confirmar, los tres se ejecutan: tres pedidos, tres cobros.
26. a) Porque solo ignora duplicados de la misma función, y cada apertura crea una lambda nueva  
   Cada vez que se ejecuta `abrirModalPago()` nace una función flecha nueva. Para el navegador son funciones distintas aunque tengan el mismo texto.
27. d) Porque `bind` devuelve una función nueva, distinta de la que se registró  
   Cada `bind` crea una función distinta. La baja busca por referencia, no encuentra la suya y no da ningún error. Por eso la solución guarda una sola función en un campo.
28. a) Registrar el listener una sola vez al cargar la página; b) Mantener el alta en la apertura y darlo de baja al cancelar con un `AbortController`  
   Alta única, o alta y baja simétricas. Desactivar el botón no quita los listeners acumulados, y una flecha escrita dentro de la función sigue naciendo nueva en cada apertura.
29. a) Un `flow.collect { }` dentro de `onResume()` sin cancelarlo  
   `onResume()` se ejecuta cada vez que la pantalla vuelve a primer plano: cada vez se añade un colector más si nadie cancela el anterior.
30. c) Las de «man» («mandarina, manzana…»), porque es la última respuesta en llegar  
   Las dos peticiones siguen vivas. La de «mango» pinta primero y la lenta de «man» pinta encima: **carrera de respuestas**.
31. `5`  
   Cinco. Cada tecla programa su propio temporizador y nadie cancela los anteriores: **retrasar no es agrupar**.
32. a) El temporizador de la tecla anterior, si aún no ha saltado; b) La petición `fetch` anterior que sigue en vuelo  
   Hay que anular el temporizador pendiente (para no lanzar búsquedas inútiles) y la petición anterior en vuelo (para que su respuesta no llegue a pintar). El listener de `input` sí debe seguir vivo.
33. 1. `clearTimeout` anula el temporizador pendiente → 2. Se programa un nuevo `setTimeout` de 300 ms → 3. Al saltar, se aborta la petición anterior con `control?.abort()` → 4. Se crea un `AbortController` nuevo → 5. Se lanza `fetch` con `{ signal: control.signal }` → 6. Se llama a `pintarSugerencias` con la respuesta  
   El debounce actúa en el listener (paso 1-2) y el abort dentro del temporizador (pasos 3-5). Si la petición se aborta, su `await` lanza `AbortError` y nunca llega a pintar.
34. d) Porque cancelar una petición a propósito no es un fallo, pero un error de red real sí debe propagarse  
   Abortar es la baja de la petición: es el comportamiento esperado. Tragarse todos los errores escondería fallos reales.
35. b) Ni sirena ni puertas: la excepción corta la ronda y sube hasta el código del detector  
   La excepción sale de `h(ev)`, interrumpe el `for` y sale por `publicar`. Los suscriptores que iban detrás nunca se enteran.
36. a) Informa de la excepción y sigue con el siguiente listener  
   El DOM informa de la excepción (en la consola) y continúa con el siguiente listener. Node `EventEmitter` y un bucle escrito a mano en Kotlin o C++ se detienen.
37. b) Node `EventEmitter`; c) Un `for` escrito a mano en Kotlin; d) Un `for` escrito a mano en C++  
   Solo el DOM sigue con el siguiente listener. Node, Kotlin y C++ propagan la excepción y cortan la ronda.
38. d) Dentro del `for`, alrededor de cada `h(ev)`  
   Un `try` alrededor de toda la ronda capturaría la excepción, pero el bucle ya se habría cortado. Tiene que envolver cada llamada para que el `for` continúe.
39. d) La lambda escribe en memoria liberada a través de un puntero colgante: basura en el display, un reinicio… o nada durante semanas  
   `[this]` no tiene dueño: la lambda guarda un puntero a un objeto que ya no existe. Es comportamiento indefinido: un fallo intermitente que depende de quién ocupe esa memoria (en un ESP32 puede acabar en un reinicio tipo *Guru Meditation*).
40. `destructor`  
   El **destructor**. Si la baja vive en un destructor, nadie tiene que acordarse de hacerla: la llama el compilador.
41. a) Para que no haya dos objetos cuyo destructor intente dar de baja la misma suscripción  
   Una suscripción tiene un único dueño. Si se copiara, la primera copia en morir daría de baja al oyente mientras la otra cree que sigue suscrito. Con `unique_ptr` y sin copia, la vida de la suscripción es exactamente la de la pantalla.
42. a) `DisposableEffect` en Compose; b) `AbortController` en el DOM; c) Qt al destruir un `QObject`  
   Los tres atan la baja al fin de vida de algo (un composable, una señal de aborto, un objeto Qt). `setTimeout` es justo lo contrario: un callback diferido que hay que cancelar a mano.
43. c) Alta en un método que se repite (sin baja simétrica)  
   Fila 6: alguien se suscribe en un método que se ejecuta muchas veces (como `onResume` o abrir un modal) y nunca da de baja. Remedio: alta única o baja simétrica.
44. b) Recorrer una foto de la lista  
   Fila 2: baja durante el recorrido de la lista viva. Los índices se desplazan y alguien se queda sin turno. Remedio: recorrer una foto.
45. a) Delegar el trabajo lento y retornar enseguida  
   Fila 11: handler lento en la ronda. El despacho es síncrono y bloquea a todos. El remedio, delegar y retornar, se desarrolla en la Unidad 3 (y la Unidad 6).
46. b) MVVM  
   El Presenter guarda `vista: ContadorView?` y le llama. El ViewModel solo expone `Observable`s; es la vista la que se suscribe.
47. a) El listener del botón: solo avisa a la lógica  
   `setOnClickListener { …onPulsar() }` es igual en los dos: la vista avisa. La diferencia está en el camino de vuelta.
48. d) Con el ViewModel solo, leyendo su estado  
   Como el ViewModel no conoce la vista, basta con llamar a sus métodos y comprobar el estado. En MVP hace falta una vista falsa porque el Presenter le llama.
49. a) Una referencia a la `Activity`; c) Una `View` concreta, como un `TextView`; d) Un `Context` de pantalla  
   `Activity`, `View` y `Context` de pantalla son la vista: guardarlos convierte el ViewModel en un MVP mal hecho y además provoca fugas, porque el ViewModel sobrevive a la rotación. El estado y el repositorio sí le pertenecen.
50. c) La nueva Activity vacía: el perfil se pinta en la Activity muerta, que además no se puede liberar (fuga)  
   `mostrarPerfil` se llama sobre la Activity destruida: pinta en una pantalla que nadie ve, la mantiene viva en memoria y la nueva se queda vacía.
51. a) …un alta implícita sin su baja  
   El Presenter «se suscribe» a la vista al guardarla: es un alta implícita. Sin una baja (soltar la vista), el callback de red la encuentra muerta.
52. d) `onStart` y `onStop`  
   `onStart { adjuntar(this) }` y `onStop { soltar() }`: un par simétrico. Mientras la pantalla no es visible, el Presenter no le habla.
53. d) Para repintar la nueva vista con el último perfil cuando se adjunta  
   Si el perfil llega con la vista soltada, se guarda en `ultimo`; `adjuntar` lo repinta. Es exactamente el «valor actual al suscribirse» de un observable.
54. b) El Toast «Pedido enviado ✔» vuelve a aparecer  
   Cada Activity nueva hace `observe()` y `LiveData` le entrega el último valor guardado: el Toast se repite en cada giro. El pedido no se reenvía (eso lo hace `onConfirmar`), pero el aviso sí.
55. a) Que la app lo vuelve a llevar a la confirmación cada vez que la pantalla del pedido se suscribe de nuevo  
   Al volver a la pantalla del pedido (girar, o regresar con «Atrás»), su `observe()` recibe otra vez el valor guardado y vuelve a navegar: el usuario queda atrapado. El mensaje no es algo que *es*, sino algo que *ha pasado*.
56. a) El número de pulsaciones del contador; c) Que el pedido está enviado (`enviado = true`); e) Que el botón está deshabilitado mientras se envía  
   Pulsaciones, `enviado` y botón deshabilitado describen cómo está la pantalla: estado. El Toast y la navegación son cosas que han pasado: eventos que se consumen una vez.
57. `Channel`  
   `Channel<String>(Channel.BUFFERED)`, expuesto como flujo con `receiveAsFlow()`. Para el estado se usa `MutableStateFlow`.
58. a) Suscribirse en `onResume()` sin dar de baja en ningún sitio; c) Publicar `pendiente-valoracion` desde dentro de la ronda de `Pedidos`; d) `desuscribir { }` con una lambda recién creada; f) Hacer una petición HTTP que puede lanzar dentro del handler, en el hilo principal  
   1) alta sin baja en un método que se repite; 2) reentrancia al publicar en el mismo sujeto; 3) baja con otra referencia; 4) red en el handler: congela la UI y, sin red, la excepción corta la ronda. Pintar es correcto y en Kotlin `==` compara contenido.
59. d) Delegar el trabajo lento fuera del hilo principal y aislar su excepción para que no corte la ronda  
   Delegar y retornar (el handler no bloquea) y aislar (un fallo de red no afecta a nadie más). Además se saca de la Activity: la analítica no depende de que la pantalla exista.
60. c) 8 · ¿Puede un suscriptor publicar? ¿Se entrega ya o se encola?  
   Es reentrancia: un suscriptor que publica (asigna un valor nuevo) en su turno abre una ronda anidada. La pregunta 8 obliga a decidir si eso se permite y si se entrega ya o se encola.
61. `baja`  
   También tiene **baja**: `clearTimeout`, `abort()`, `cancel()`. Quien da de alta guarda lo que necesita para dar de baja.
62. d) La cola `pendientes` con su `while` (arreglo B)  
   Productor → cola → dispatcher → handler, con run-to-completion. La Unidad 3 lo completa con prioridades y estudia el handler lento.

## Predice, ejecuta, diagnostica

_Cinco escenas con la central de pedidos: predice la salida línea a línea, compárala con la real y diagnostica el fallo con la ficha y la lista de diez preguntas._

### La central de pedidos y el método · Teoría

Hoy no hay ningún concepto nuevo: **los fallos los encontráis vosotros**. Todas las escenas usan la misma central de pedidos, que es el `Sensor` de la clase 1 con otro nombre.

```kotlin
// CentralPedidos.kt
class CentralPedidos {
    private val oyentes = mutableListOf<OyentePedido>()

    fun suscribir(o: OyentePedido) { oyentes += o }       // alta
    fun desuscribir(o: OyentePedido) { oyentes -= o }     // baja
    fun cuantos(): Int = oyentes.size

    // Una ronda: avisa a todos los suscriptores, uno detrás de otro.
    fun publicar(estado: String) {
        for (o in oyentes.toList()) {
            o(estado)
        }
    }
}
```

`OyentePedido` es un tipo función: recibe el estado del pedido (un `String`) y no devuelve nada. Mira bien la línea del `for`: hoy vuelve a salir.

### El método

<div class="flow">
  <div><b>1 · Predecir</b><small>En papel, la salida completa, línea a línea. Antes de ejecutar.</small></div>
  <div><b>2 · Ejecutar</b><small>El archivo de la escena, tal como está. ¿Qué línea no esperabais?</small></div>
  <div><b>3 · Diagnosticar</b><small>Síntoma, fila de la ficha, remedio y pregunta de la lista que nadie se hizo.</small></div>
</div>

> [!IMPORTANTE] El código no se toca
> Si la pantalla no dice lo que habías escrito, **has encontrado el fallo que se busca**. Por eso la predicción tiene que ir antes: si ejecutas primero, cualquier salida te parecerá razonable.

> [!ANALOGIA] El médico
> Un buen diagnóstico no empieza por la radiografía, sino por la hipótesis: «si es esto, debería ver aquello». La salida es tu radiografía; tu predicción, la hipótesis.

En esta sala cada escena tiene dos pasos: primero predices (con el código o el escenario delante) y después, en el paso siguiente, ves la salida real y haces el diagnóstico.

**Preguntas**

**1.** ¿`publicar` recorre la lista viva o una foto?

   a) La lista viva: `oyentes` se recorre directamente
   b) Una foto: `toList()` crea una copia antes de recorrerla
   c) Depende de si hay altas durante la ronda
   d) Ninguna: Kotlin recorre las listas en paralelo

**2.** Ordena los pasos del método de la clase.

   Ordena: Anotar síntoma, fila de la ficha, remedio y pregunta de la lista · Buscar la primera línea que no coincide · Ejecutar el archivo tal como está · Escribir en papel la salida esperada, línea a línea

### Las dos herramientas: ficha y lista · Teoría

Para diagnosticar usaremos las dos herramientas de la clase anterior. Te las dejamos numeradas, porque cada escena se resolverá citando **una fila** y **una pregunta**.

### Ficha de diagnóstico

| Fila | Síntoma que se observa | Antipatrón |
| --- | --- | --- |
| 1 | El resultado depende del orden de carga | Suscriptores que dependen unos de otros |
| 2 | Un suscriptor nunca se entera, sin error | Baja durante el recorrido de la lista viva |
| 3 | Un suscriptor dado de baja reacciona una última vez | Foto sin comprobar si sigue activo |
| 4 | La vista muestra un valor distinto del modelo | Reentrancia: un suscriptor publica en su turno |
| 5 | Desbordamiento de pila o tráfico infinito | Ciclo de notificación |
| 6 | La acción se ejecuta 2, 3… N veces | Alta en un método que se repite |
| 7 | Pantalla cerrada que reacciona; memoria que crece | Baja con otra referencia |
| 8 | Aparecen resultados de una búsqueda anterior | Callback diferido sin cancelar |
| 9 | Tras un fallo, el resto no reacciona | Excepción sin aislar |
| 10 | Reinicios aleatorios en C/C++ | Puntero colgante en el callback |
| 11 | La interfaz se congela mientras un handler trabaja | Handler lento en la ronda |

### Lista de diez preguntas

| # | Pregunta |
| --- | --- |
| 1 | ¿Cuál es el contrato del callback? |
| 2 | ¿Síncrono o diferido? |
| 3 | ¿Cuántos suscriptores? |
| 4 | ¿Quién da de baja? ¿Con la misma referencia que el alta? |
| 5 | ¿Cómo se asocia evento → handler? |
| 6 | ¿De dónde saca el handler sus datos? |
| 7 | ¿Qué pasa con las altas y bajas durante una ronda? |
| 8 | ¿Puede un suscriptor publicar? ¿Se entrega ya o se encola? |
| 9 | ¿Qué pasa si un suscriptor falla o tarda? ¿Quién se entera? |
| 10 | ¿Es un estado o un evento? |

> [!TRUCO]
> A veces el síntoma está en una fila y la causa en otra: «la acción se ejecuta tres veces» (fila 6) puede deberse a una baja que no da de baja nada (fila 7).

**Preguntas**

**3.** ¿Qué número de fila de la ficha corresponde a la **reentrancia**?

**4.** Un suscriptor lanza una excepción y los que van detrás no reaccionan. ¿Qué pregunta de la lista nadie se hizo?

   a) 9 · ¿Qué pasa si un suscriptor falla o tarda?
   b) 2 · ¿Síncrono o diferido?
   c) 6 · ¿De dónde saca el handler sus datos?
   d) 10 · ¿Es un estado o un evento?

### Escena 1 · ¿Qué imprime? · Ejercicio

La escena 1 se hace entre todos y sirve de modelo. Tres minutos, sin ejecutar.

```kotlin
// Escena1Valoracion.kt
fun main() {
    val central = CentralPedidos()
    val historial = mutableListOf<String>()

    // Suscriptor 1: la app del cliente
    central.suscribir { estado ->
        println("  App: $estado")
        if (estado == "entregado") central.publicar("pendiente de valoracion")
    }

    // Suscriptor 2: el historial del pedido
    central.suscribir { estado ->
        println("  Historial: $estado")
        historial += estado
    }

    println("Escena 1")
    central.publicar("en camino")
    central.publicar("entregado")
    println("Historial guardado: $historial")
}
```

Tres cosas en las que fijarse:

1. **Suscriptor 1, la app**: pinta el estado. Mira lo que hace cuando recibe `entregado`.
2. **Suscriptor 2, el historial**: pinta y guarda cada estado, en el orden en que le llega.
3. **Dos avisos seguidos**: `en camino` y `entregado`. ¿Qué queda guardado?

> [!TRUCO] Cómo apuntar las rondas
> Escribe cada `publicar` como un bloque sangrado. Si dentro de un turno se llama a `publicar`, abre un bloque **más sangrado** dentro del actual y termínalo entero antes de volver al de fuera. La salida tiene **ocho líneas**.

**Preguntas**

**5.** Ordena las ocho líneas de la salida de `Escena1Valoracion.kt`.

   Ordena: App: en camino · App: entregado · App: pendiente de valoracion · Escena 1 · Historial guardado: [en camino, pendiente de valoracion, entregado] · Historial: en camino · Historial: entregado · Historial: pendiente de valoracion

**6.** ¿Cuántas rondas se abren en total y cuántas de ellas son anidadas?

   a) 2 rondas, ninguna anidada
   b) 3 rondas, 1 anidada
   c) 3 rondas, ninguna anidada
   d) 4 rondas, 2 anidadas

### Escena 1 · Diagnóstico y dos arreglos · Ejercicio

Lo que sale: una ronda dentro de otra, y **el historial recibe los avisos al revés**.

```text
RONDA 1 · publicar("entregado")
  1 La app recibe entregado
      RONDA 2 · publicar("pendiente de valoracion")
        2 La app recibe pendiente de valoracion
        3 El historial recibe pendiente de valoracion
  4 El historial recibe entregado      ← estado viejo
```

La ronda 1 sigue donde estaba: su último aviso es un estado viejo. Aquí hay **un solo hilo**: la reentrancia es una llamada dentro de otra, no un problema de concurrencia.

### Diagnóstico

| Síntoma | Fila de la ficha | Pregunta de la lista |
| --- | --- | --- |
| El historial guarda los estados en un orden que no ocurrió | 4 · Reentrancia: un suscriptor publica en el mismo sujeto durante su turno | 8 · ¿Puede un suscriptor publicar? ¿Se entrega ya o se encola? |

### Arreglo A · la app no republica

```kotlin
if (estado == "entregado")
    println("  App: que tal tu pedido?")
// Historial guardado: [en camino, entregado]
```

### Arreglo B · una palabra: la central entrega con cola

```kotlin
val central = CentralConCola()
```

`CentralConCola` tiene una cola `pendientes` y una bandera `notificando`, como el `Observable` con entrega diferida de la clase 4: si alguien publica durante una ronda, el aviso **espera en la cola** a que termine. Con B no se toca a ningún suscriptor. Guarda esa imagen para el final.

![Arriba, CentralPedidos: en la ronda 1 de entregado, la app recibe entregado y publica, y la ronda 2 de pendiente de valoracion se abre dentro: la reciben la app y el historial; al volver, el historial recibe entregado, un estado viejo, y guarda en camino, pendiente de valoracion, entregado. Abajo, CentralConCola: el aviso de la app va a la cola, la ronda 1 termina entregando entregado al historial y después la ronda 2 entrega pendiente de valoracion; el historial guarda en camino, entregado, pendiente de valoracion, el orden real](/img/pde/tema-2/2b-escena1-rondas.svg "Escena 1: la ronda anidada desordena el historial; con cola, cada ronda termina antes de la siguiente")

**Preguntas**

**7.** El historial dice que el pedido quedó pendiente de valoración **antes** de entregarse. ¿Quién ha mentido?

   a) La app, porque imprime los estados mal
   b) El historial, que guarda la lista en orden inverso
   c) Nadie a propósito: la central entrega la ronda 2 dentro de la ronda 1 y el historial guarda en el orden en que le llega
   d) Kotlin, porque ejecuta los suscriptores en varios hilos

**8.** Con el arreglo B (`CentralConCola`), ordena lo que acaba guardado en el historial.

   Ordena: en camino · entregado · pendiente de valoracion

### Escena 2 · El timbre · Ejercicio

Desde aquí, en parejas, unos cinco minutos por escena. La central es la del principio, **sin los arreglos** de la escena 1.

**Escenario.** El cliente tiene activado el modo «no molestar». Cuando el repartidor llega a la puerta, el suscriptor de «no molestar» da de baja al timbre. Hay tres suscriptores, en este orden: no molestar, timbre e historial. **¿Suena el timbre?**

En clase solo se proyectan la salida y el arreglo. Este es un código **en versión simplificada**, coherente con esa salida:

```kotlin
// Escena2Timbre.kt (versión simplificada)
fun main() {
    val central = CentralPedidos()
    val timbre: OyentePedido = { estado ->
        if (estado == "en la puerta") println("  Timbre: DING DONG")
    }
    val noMolestar: OyentePedido = { estado ->
        if (estado == "en la puerta") {
            println("  No molestar: se da de baja el timbre")
            central.desuscribir(timbre)
        }
    }
    central.suscribir(noMolestar)
    central.suscribir(timbre)
    central.suscribir { estado -> println("  Historial: $estado") }

    println("Escena 2")
    central.publicar("en la puerta")
    println("Suscriptores: ${central.cuantos()}")
    central.publicar("entregado")
}
```

Recuerda lo que respondiste en el primer paso: `publicar` recorre `oyentes.toList()`.

**Preguntas**

**9.** ¿Aparece la línea `Timbre: DING DONG`?

   a) No: la baja se hace antes de que le llegue el turno al timbre
   b) Sí: el timbre estaba en la foto de la ronda y la central no comprueba si sigue suscrito
   c) No: salta `ConcurrentModificationException`
   d) Sí, y además suena otra vez con `entregado`

**10.** ¿Qué número imprime la línea `Suscriptores: …`?

### Escena 2 · El timbre suena después de la baja · Ejercicio

Salida real:

```text
Escena 2
  No molestar: se da de baja el timbre
  Timbre: DING DONG
  Historial: en la puerta
Suscriptores: 2
  Historial: entregado
```

El cliente pidió no ser molestado y el timbre suena igualmente. En la segunda ronda (`entregado`) el timbre ya no está, porque la foto nueva se hace sin él.

| Síntoma | Fila | Pregunta |
| --- | --- | --- |
| Un suscriptor dado de baja reacciona una última vez | 3 · Foto sin comprobar si el suscriptor sigue activo | 7 · ¿Qué pasa con las altas y bajas durante una ronda? |

### Arreglo en `CentralPedidos.kt`: foto, y comprobar que sigue suscrito

```kotlin
for (o in oyentes.toList()) {
    if (o in oyentes) o(estado)
}
// con el arreglo, la línea «Timbre: DING DONG» desaparece
```

La foto protege el bucle; la comprobación `o in oyentes` hace que **una baja cuente desde ya**. Es la misma idea que la bandera `activa` del GPS de la clase 4, sin necesidad de una clase `Suscripcion`.

> [!IMPORTANTE]
> Ninguna política es «la correcta». Node entrega el aviso en curso; el DOM no. **Lo incorrecto es no saber cuál ofrece vuestra central.** Aquí el «no molestar» necesita la segunda.

**Preguntas**

**11.** ¿Qué comprueba `if (o in oyentes)` dentro del bucle?

   a) Que el suscriptor de la foto siga en la lista real en el momento de su turno
   b) Que `o` no sea `null`
   c) Que la lista no se haya vaciado
   d) Que `o` no se haya suscrito dos veces

**12.** Sin el arreglo, ¿a qué plataforma se parece la `CentralPedidos` original en esta pregunta del contrato?

   a) Al DOM
   b) A Node `EventEmitter`
   c) A ninguna: es un fallo de Kotlin
   d) A los dos por igual

### Escena 3 · La pantalla · Ejercicio

**Escenario.** El usuario entra en la pantalla de seguimiento, sale, vuelve a entrar, vuelve a salir y entra por tercera vez. Estando dentro, el pedido pasa a «en camino». **¿Cuántas veces se pinta el aviso?**

Versión simplificada, coherente con la salida que se vio en clase:

```kotlin
// Escena3Pantalla.kt (versión simplificada)
class PantallaSeguimiento(private val central: CentralPedidos) {
    private fun pintar(estado: String) = println("  Pantalla: $estado")

    fun alVolver() { central.suscribir { estado -> pintar(estado) } }
    fun alSalir()  { central.desuscribir { estado -> pintar(estado) } }
}

fun main() {
    val central = CentralPedidos()
    val pantalla = PantallaSeguimiento(central)
    println("Escena 3")
    pantalla.alVolver(); pantalla.alSalir()   // 1.ª visita
    pantalla.alVolver(); pantalla.alSalir()   // 2.ª visita
    pantalla.alVolver()                       // 3.ª visita: sigue dentro
    central.publicar("en camino")
    println("Suscriptores: ${central.cuantos()}")
}
```

Hay alta y hay baja, en métodos simétricos. A primera vista está bien. Fíjate en **qué objeto** se pasa a `suscribir` y **qué objeto** se pasa a `desuscribir`.

**Preguntas**

**13.** ¿Cuántas líneas `Pantalla: en camino` se imprimen?

**14.** ¿Da algún error `desuscribir` cuando la lambda no está en la lista?

   a) Sí, `NoSuchElementException`
   b) Sí, `ConcurrentModificationException`
   c) No: `oyentes -= o` simplemente no quita nada y sigue
   d) No compila: no se puede pasar una lambda a `desuscribir`

### Escena 3 · Un aviso, tres repintados · Ejercicio

Salida real:

```text
Escena 3
  Pantalla: en camino
  Pantalla: en camino
  Pantalla: en camino
Suscriptores: 3
```

| Síntoma (fila 6) | Causa (fila 7) | Pregunta |
| --- | --- | --- |
| La acción se ejecuta N veces | Baja con otra referencia | 4 · ¿Quién da de baja? ¿Con la misma referencia que el alta? |

Aquí el síntoma está en una fila y la causa en otra. **Dos lambdas con el mismo texto son dos objetos.** La baja buscó un objeto que nunca estuvo en la lista, y no dijo nada. En una app real, además del triple repintado, cada pantalla «cerrada» sigue viva en memoria a través de su lambda.

### Arreglo: una sola referencia, guardada

```kotlin
private val oyente: OyentePedido = { estado -> pintar(estado) }  // propiedad nueva

central.suscribir(oyente)       // en alVolver
central.desuscribir(oyente)     // en alSalir

// con el arreglo: una línea «Pantalla: en camino» y «Suscriptores: 1»
```

La lambda se crea **una vez**, al construir la pantalla, y se guarda en una propiedad. El alta y la baja usan la misma referencia.

> [!NOTA] Es el mismo fallo del feed
> En la clase 4 lo vimos en JavaScript con `bind`: `removeEventListener("scroll", this.cargarMas.bind(this))` tampoco quitaba nada. Cambia el lenguaje, no el fallo.

**Preguntas**

**15.** ¿Qué imprime la escena 3 con el arreglo?

   a) Ninguna línea de pantalla y `Suscriptores: 0`
   b) Tres líneas y `Suscriptores: 1`
   c) Una línea y `Suscriptores: 3`
   d) Una línea `Pantalla: en camino` y `Suscriptores: 1`

**16.** Completa: «el alta y la baja deben usar la misma …»

### Escena 4 · La cocina · Ejercicio

**Escenario.** Restaurante con reparto. Cuando un pedido pasa a `pagado`, tres suscriptores reaccionan en este orden: la app del cliente, la **analítica** y la **cocina**. Hoy la analítica no tiene red y lanza una excepción. **¿Se entera la cocina?**

Versión simplificada, coherente con la salida que se vio en clase:

```kotlin
// Escena4Cocina.kt (versión simplificada)
fun main() {
    val central = CentralPedidos()
    central.suscribir { estado -> println("  App: $estado") }
    central.suscribir { estado ->                         // analítica: hoy sin red
        throw IllegalStateException("analitica sin red ($estado)")
    }
    central.suscribir { estado ->
        if (estado == "pagado") println("  Cocina: a preparar el pedido")
    }

    println("Escena 4")
    central.publicar("pagado")
    println("Fin de la escena 4")
}
```

Recuerda cómo es el bucle de `publicar`: un `for` sobre la foto que llama a `o(estado)`. ¿Hay algún `try` en algún sitio?

> [!TRUCO]
> Cuando una predicción incluye una excepción, escribe también **hasta dónde sube**: qué funciones atraviesa y si alguien la captura. Eso decide qué líneas ya no se imprimen.

**Preguntas**

**17.** ¿Qué se imprime después de `App: pagado`?

   a) `Cocina: a preparar el pedido` y `Fin de la escena 4`
   b) Solo `Fin de la escena 4`
   c) La traza de la excepción: ni la cocina ni el «Fin» llegan a imprimirse
   d) `Cocina: a preparar el pedido` y después la traza de la excepción

**18.** ¿De quién es la responsabilidad de que el fallo de la analítica no afecte a la cocina?

   a) De la cocina, que debería suscribirse la primera
   b) De `main`, que debería envolver `publicar` en un `try`
   c) De la central, que decide qué pasa cuando un suscriptor falla
   d) De nadie: es inevitable

### Escena 4 · Falla la analítica y lo paga la cocina · Ejercicio

Salida real (el texto rojo):

```text
Escena 4
  App: pagado
Exception in thread "main" java.lang.IllegalStateException: analitica sin red (pagado)
    at Escena4CocinaKt.main$lambda$1(Escena4Cocina.kt:11)
    at CentralPedidos.publicar(CentralPedidos.kt:18)
    at Escena4CocinaKt.main(Escena4Cocina.kt:19)
    at Escena4CocinaKt.main(Escena4Cocina.kt)
```

Lee la traza de abajo arriba: `main` llama a `publicar` (línea 19), `publicar` llama a la lambda de la analítica (línea 18 de la central) y la lambda lanza. La línea de `CentralPedidos.publicar` es la que delata dónde **debería** haberse detenido la excepción.

| Síntoma | Fila | Pregunta |
| --- | --- | --- |
| Falla la analítica y la cocina no se entera | 9 · Excepción sin aislar | 9 · ¿Qué pasa si un suscriptor falla o tarda? |

### Arreglo en `CentralPedidos.kt`: aislar a cada suscriptor

```kotlin
try {
    if (o in oyentes) o(estado)
} catch (e: Exception) {
    println("  [Central] un suscriptor ha fallado: ${e.message}")
}
```

(El `if (o in oyentes)` es el arreglo de la escena 2, que se conserva.) Salida con el arreglo:

```text
Escena 4
  App: pagado
  [Central] un suscriptor ha fallado: analitica sin red (pagado)
  Cocina: a preparar el pedido
Fin de la escena 4
```

> [!CUIDADO] Aislar también es decidir quién se entera
> Con el arreglo, ¿quién sabe que la analítica ha fallado? **Solo quien lea ese mensaje.** Ni el que publicó ni la propia analítica lo saben. Por eso en la clase 4 la centralita no se limitaba a capturar: llamaba a `registrarError`.

**Preguntas**

**19.** ¿Qué línea de la traza indica por dónde atravesó la excepción la central sin detenerla?

   a) `at CentralPedidos.publicar(CentralPedidos.kt:18)`
   b) `at Escena4CocinaKt.main(Escena4Cocina.kt)`
   c) `Exception in thread "main"`
   d) `at Escena4CocinaKt.main$lambda$1(Escena4Cocina.kt:11)`

**20.** Con el arreglo, ¿quién se entera de que la analítica ha fallado?

   a) Quien lea la salida de la consola (el mensaje de la central)
   b) El código que llamó a `publicar`
   c) La propia analítica, que reintentará
   d) La cocina

### Escena 5 · ¿En qué orden salen las líneas? · Ejercicio

Esta escena la ejecuta el profesor; portátiles cerrados. Cuatro minutos.

```kotlin
// Escena5Buscador.kt
fun main() {
    val reloj = Timer()
    var enPantalla = "(nada)"

    // Cada vez que el usuario cambia el texto se lanza una búsqueda
    fun alEscribir(texto: String) {
        println("Buscador: el usuario ha escrito '$texto'")
        buscarEnTienda(reloj, texto) { resultado ->
            enPantalla = resultado
            println("  Pantalla (hilo ${Thread.currentThread().name}): $resultado")
        }
    }

    println("Escena 5")
    alEscribir("man")
    alEscribir("mango")
    println("main (hilo ${Thread.currentThread().name}): sigue sin esperar")

    Thread.sleep(1000)  // deja tiempo a que lleguen las respuestas
    reloj.cancel()
    println("El usuario ha escrito 'mango' y ve: $enPantalla")
}
```

`buscarEnTienda` **no devuelve el resultado: devuelve la tarea programada** (`TimerTask`). El resultado llega más tarde, por el callback. «man» tarda **600 ms** en contestar; «mango», **200 ms**. Una versión simplificada de esa función, coherente con la escena:

```kotlin
fun buscarEnTienda(reloj: Timer, texto: String, alLlegar: (String) -> Unit): TimerTask {
    val tarea = object : TimerTask() {
        override fun run() = alLlegar("resultados de '$texto'")
    }
    reloj.schedule(tarea, if (texto == "man") 600L else 200L)
    return tarea
}
```

**Preguntas**

**21.** Ordena las líneas de la salida de `Escena5Buscador.kt`.

   Ordena: Buscador: el usuario ha escrito 'man' · Buscador: el usuario ha escrito 'mango' · El usuario ha escrito 'mango' y ve: resultados de 'man' · Escena 5 · Pantalla (hilo Timer-0): resultados de 'man' · Pantalla (hilo Timer-0): resultados de 'mango' · main (hilo main): sigue sin esperar

**22.** ¿En qué hilo se ejecuta el callback que pinta los resultados?

   a) En `main`, cuando termina `Thread.sleep`
   b) En un hilo nuevo por cada búsqueda
   c) En el hilo del `Timer` (`Timer-0`)
   d) En el mismo hilo que llamó a `alEscribir`

### Escena 5 · Llega tarde la búsqueda vieja · Ejercicio

Lo que ve el usuario: **ha escrito «mango» y en la pantalla están los resultados de «man»**.

<div class="cards">
  <div class="card"><h4>Diferido (clase 1)</h4><p><code>main</code> sigue sin esperar. El callback corre después, y en otro hilo: <code>Timer-0</code>.</p></div>
  <div class="card"><h4>Gana la última en llegar</h4><p>No la última que se pidió. Las dos búsquedas siguen vivas.</p></div>
  <div class="card"><h4>Fila 8 · preguntas 2 y 4</h4><p>Callback diferido sin cancelar. ¿Síncrono o diferido? ¿Quién da de baja?</p></div>
</div>

¿Qué hace `alEscribir` con lo que devuelve `buscarEnTienda`? **Lo tira.** Sin la tarea en la mano no hay nada que cancelar.

### Arreglo: guardar la tarea y cancelar la anterior

```kotlin
var pendiente: TimerTask? = null

fun alEscribir(texto: String) {
    println("Buscador: el usuario ha escrito '$texto'")
    pendiente?.cancel()
    pendiente = buscarEnTienda(reloj, texto) { resultado ->
        // … igual que antes
    }
}
```

Salida con el arreglo:

```text
Escena 5
Buscador: el usuario ha escrito 'man'
Buscador: el usuario ha escrito 'mango'
main (hilo main): sigue sin esperar
  Pantalla (hilo Timer-0): resultados de 'mango'
El usuario ha escrito 'mango' y ve: resultados de 'mango'
```

![Dos líneas temporales de 0 a 1000 ms. Sin cancelar: la búsqueda de 'mango' contesta a los 200 ms y pinta 'mango'; la de 'man' contesta a los 600 ms y pinta 'man' encima, así que enPantalla pasa de (nada) a 'mango' y después a 'man'. Con pendiente?.cancel(): la búsqueda de 'man' se cancela al escribir 'mango', solo pinta 'mango' a los 200 ms y enPantalla se queda en 'mango'](/img/pde/tema-2/2b-carrera-buscador.svg "Carrera de respuestas: gana la última en llegar, salvo que se cancele la tarea anterior")

> [!IMPORTANTE] Regla
> **Un callback diferido es una suscripción de un solo uso: también tiene alta y también tiene baja.** Quien da de alta guarda lo que necesita para dar de baja.

> [!NOTA] Compáralo con la escena 3
> Allí la baja fallaba por no guardar la lambda. Aquí, por no guardar la tarea. Es el mismo error con distinta ropa.

**Preguntas**

**23.** ¿Qué método de `TimerTask` se llama para anular la búsqueda anterior?

**24.** ¿Qué tienen en común el fallo de la escena 3 y el de la escena 5?

   a) Los dos son reentrancia
   b) Los dos lanzan una excepción silenciosa
   c) Los dos se arreglan con una foto de la lista
   d) En los dos, quien da de alta no guarda lo que necesita para dar de baja (la lambda / la tarea)

### Repaso: cinco fallos, cinco preguntas · Repaso

| Escena | Lo que se ve | Fila | Remedio | Pregunta |
| --- | --- | --- | --- | --- |
| 1 · La valoración | El historial guarda un orden que no ocurrió | 4 | No republicar, o entregar con cola | 8 |
| 2 · El timbre | Suena después de darlo de baja | 3 | Foto, y comprobar que sigue suscrito | 7 |
| 3 · La pantalla | Un aviso, tres repintados | 6 y 7 | La misma referencia en alta y baja | 4 |
| 4 · La cocina | Falla la analítica y la cocina no se entera | 9 | `try/catch` por suscriptor | 9 |
| 5 · El buscador | Llegan resultados de una búsqueda anterior | 8 | Guardar la tarea y cancelarla | 2 y 4 |

> [!IMPORTANTE]
> **Cuatro de los cinco programas terminan sin ningún error.** Solo se ven si alguien predice la salida antes de ejecutar.

### Cinco preguntas para el parcial

Intenta contestarlas en voz alta (treinta segundos cada una) antes de abrir la respuesta.

<details><summary>1 · Un suscriptor dado de baja durante una ronda recibe el aviso en curso. ¿Es un fallo?</summary>
<p>No necesariamente: es la política de <b>foto pura</b> (la de Node <code>EventEmitter</code>). Es un fallo si el contrato promete lo contrario (como el DOM) o si nadie sabe qué política ofrece la central, como el «no molestar» de la escena 2.</p>
</details>

<details><summary>2 · ¿Por qué desuscribir { estado -&gt; pintar(estado) } no da de baja nada y tampoco da error?</summary>
<p>Porque esa lambda acaba de nacer: es un objeto distinto del que se dio de alta, aunque tenga el mismo texto. Quitar de la lista algo que no está no es un error; simplemente no quita nada.</p>
</details>

<details><summary>3 · En la escena 1, ¿qué recibe el historial en último lugar y por qué es un problema?</summary>
<p>Recibe <code>entregado</code>, un estado ya viejo: la ronda anidada de <code>pendiente de valoracion</code> se entregó antes. Quien lea el último estado del historial se equivoca.</p>
</details>

<details><summary>4 · Con el try/catch por suscriptor, ¿quién se entera de que la analítica ha fallado?</summary>
<p>Solo quien lea el mensaje que imprime la central. Ni el publicador ni la analítica lo saben: aislar también es decidir quién se entera.</p>
</details>

<details><summary>5 · ¿En qué se parecen cancelar un TimerTask y dar de baja un listener?</summary>
<p>Son lo mismo: un callback diferido es una suscripción de un solo uso. En ambos casos hay que guardar la referencia (tarea o lambda) en el alta para poder dar de baja.</p>
</details>

**Preguntas**

**25.** ¿Cuál de las cinco escenas es la única que termina con una excepción visible?

   a) Escena 1 · La valoración
   b) Escena 3 · La pantalla
   c) Escena 4 · La cocina
   d) Escena 5 · El buscador

**26.** Según la tabla, ¿qué fila de la ficha y qué preguntas de la lista corresponden a la escena del buscador?

   a) Fila 4 · pregunta 8
   b) Fila 9 · pregunta 9
   c) Fila 3 · pregunta 7
   d) Fila 8 · preguntas 2 y 4

**27.** Completa la regla común a las escenas 3 y 5: «Quien da de alta guarda lo que necesita para dar de …»

### Repaso final: lo que queda para la Unidad 3 · Repaso

> [!RESUMEN] Lo que te llevas de esta sala
> - **Predice antes de ejecutar**: cuatro de los cinco fallos no dan ningún error.
> - **Escena 1**: un suscriptor que publica en su turno abre una ronda anidada (reentrancia).
> - **Escena 2**: la foto protege el bucle, pero la baja solo cuenta desde ya si se comprueba (`o in oyentes`).
> - **Escena 3**: dos lambdas con el mismo texto son dos objetos; la baja necesita la misma referencia.
> - **Escena 4**: sin `try/catch` por suscriptor, el fallo de uno lo pagan los demás; y aislar es decidir quién se entera.
> - **Escena 5**: un callback diferido también tiene baja: guarda la `TimerTask` y cancélala.

### Lo que queda para la Unidad 3

Recuerda el arreglo B de la escena 1: cambiar una palabra para usar `CentralConCola`. Este es su `publicar`:

```kotlin
// CentralPedidos.kt · class CentralConCola
fun publicar(estado: String) {
    pendientes.addLast(estado)
    if (notificando) return
    notificando = true
    try {
        while (pendientes.isNotEmpty()) {
            val siguiente = pendientes.removeFirst()
            for (o in oyentes.toList()) {
                o(siguiente)
            }
        }
    } finally {
        notificando = false
    }
}
```

<div class="cards">
  <div class="card"><h4>1 · La cola</h4><p><code>pendientes</code> guarda los avisos que llegan durante una ronda.</p></div>
  <div class="card"><h4>2 · Quién la vacía</h4><p>Quien publicó primero: se queda hasta entregarlo todo.</p></div>
  <div class="card"><h4>3 · El bucle</h4><p>El <code>while</code> entrega los avisos uno a uno. Cada ronda termina antes de la siguiente.</p></div>
</div>

> [!NOTA] Para la Unidad 3
> **¿Y si quien publica dejara el aviso y se fuera?** Alguien tendría que vaciar la cola por él: ese alguien es el bucle de eventos.

**Preguntas**

**28.** En `CentralConCola`, ¿quién vacía la cola `pendientes`?

   a) Un hilo aparte que la revisa cada cierto tiempo
   b) El último suscriptor de cada ronda
   c) Quien publicó primero: se queda en el `while` hasta entregarlo todo
   d) Cada llamada a `publicar`, incluidas las reentrantes

**29.** Ordena lo que hace `CentralConCola.publicar` en la primera llamada (la que no es reentrante).

   Ordena: Añade el aviso al final de `pendientes` · Comprueba `notificando` y, como es `false`, sigue · Con la cola vacía, pone `notificando = false` en el `finally` · Lo entrega a la foto de suscriptores · Pone `notificando = true` · Saca el primer aviso de la cola

### Solucionario

1. b) Una foto: `toList()` crea una copia antes de recorrerla  
   `oyentes.toList()` devuelve una lista nueva con los mismos elementos. Las altas y bajas durante la ronda no rompen el bucle… pero tampoco afectan a quién recibe el aviso en curso. Tenlo en mente para la escena 2.
2. 1. Escribir en papel la salida esperada, línea a línea → 2. Ejecutar el archivo tal como está → 3. Buscar la primera línea que no coincide → 4. Anotar síntoma, fila de la ficha, remedio y pregunta de la lista  
   Predecir → ejecutar → diagnosticar. La línea que no coincide con la predicción es el síntoma.
3. `4`  
   Fila 4: la vista (o el historial) muestra algo distinto de lo que pasó porque un suscriptor publica durante su turno.
4. a) 9 · ¿Qué pasa si un suscriptor falla o tarda?  
   La pregunta 9, que se corresponde con la fila 9 de la ficha: excepción sin aislar.
5. 1. Escena 1 → 2. App: en camino → 3. Historial: en camino → 4. App: entregado → 5. App: pendiente de valoracion → 6. Historial: pendiente de valoracion → 7. Historial: entregado → 8. Historial guardado: [en camino, pendiente de valoracion, entregado]  
   La ronda de `entregado` llama primero a la app, que abre una ronda anidada con `pendiente de valoracion`: la app y el historial la reciben completa. Solo después la ronda de fuera continúa y el historial recibe `entregado`, ya viejo.
6. b) 3 rondas, 1 anidada  
   `en camino`, `entregado` y `pendiente de valoracion`; esta última empieza dentro de la ronda de `entregado`, antes de que termine.
7. c) Nadie a propósito: la central entrega la ronda 2 dentro de la ronda 1 y el historial guarda en el orden en que le llega  
   El historial es fiel a lo que recibe; el problema es el orden de entrega cuando un suscriptor publica en su turno (reentrancia). Es una pregunta del contrato de la central, no de un suscriptor.
8. 1. en camino → 2. entregado → 3. pendiente de valoracion  
   `Historial guardado: [en camino, entregado, pendiente de valoracion]`. La app sigue publicando, pero el aviso se entrega cuando acaba la ronda en curso: el orden vuelve a ser el real.
9. b) Sí: el timbre estaba en la foto de la ronda y la central no comprueba si sigue suscrito  
   El timbre está en la foto que se hizo al empezar la ronda. La baja lo quita de `oyentes`, pero la foto no cambia: el timbre suena después de haberlo dado de baja.
10. `2`  
   2: no molestar e historial. La baja del timbre sí se ha hecho en la lista real; lo que no ha hecho es impedir el aviso en curso.
11. a) Que el suscriptor de la foto siga en la lista real en el momento de su turno  
   La foto decide quién *podría* recibir; la lista real, quién *sigue* suscrito. Si alguien se dio de baja antes de su turno, no se le llama.
12. b) A Node `EventEmitter`  
   Foto pura: el suscriptor dado de baja durante la ronda recibe el aviso en curso, como en Node. Con el arreglo se comporta como el DOM.
13. `3`  
   Tres. Cada `alVolver` añade una lambda nueva; cada `alSalir` intenta quitar otra lambda, también nueva, que nunca estuvo en la lista.
14. c) No: `oyentes -= o` simplemente no quita nada y sigue  
   Quitar de una lista un elemento que no está no es un error en Kotlin: no hace nada. Por eso este fallo es silencioso.
15. d) Una línea `Pantalla: en camino` y `Suscriptores: 1`  
   Las dos primeras visitas dan de alta y de baja la misma referencia; en la tercera el usuario sigue dentro, así que queda exactamente un suscriptor.
16. `referencia`  
   La misma **referencia**: el objeto que se dio de alta es el que hay que dar de baja.
17. c) La traza de la excepción: ni la cocina ni el «Fin» llegan a imprimirse  
   La excepción de la analítica corta la ronda: la cocina va detrás y no recibe el aviso. Como nadie la captura, también corta `main` y el programa termina con la traza.
18. c) De la central, que decide qué pasa cuando un suscriptor falla  
   Es la pregunta 4 de la letra pequeña: «si un suscriptor lanza, ¿se sigue avisando a los demás?». La respuesta la da el bucle de la central. Reordenar suscriptores o capturar en `main` no hace que la cocina se entere.
19. a) `at CentralPedidos.publicar(CentralPedidos.kt:18)`  
   La línea 18 de la central es la llamada `o(estado)` dentro del `for`: ahí es donde debe ir el `try/catch` por suscriptor.
20. a) Quien lea la salida de la consola (el mensaje de la central)  
   Solo quien lea el mensaje. `publicar` retorna con normalidad, la analítica no recibe ninguna señal y la cocina ni lo sabe ni le importa. Aislar sin informar bien es esconder el fallo.
21. 1. Escena 5 → 2. Buscador: el usuario ha escrito 'man' → 3. Buscador: el usuario ha escrito 'mango' → 4. main (hilo main): sigue sin esperar → 5. Pantalla (hilo Timer-0): resultados de 'mango' → 6. Pantalla (hilo Timer-0): resultados de 'man' → 7. El usuario ha escrito 'mango' y ve: resultados de 'man'  
   Las tres primeras líneas de `main` salen de inmediato (callback diferido). A los 200 ms llega «mango» y a los 600 ms «man», que sobrescribe `enPantalla`. Al final de `main` el usuario ve los resultados de «man».
22. c) En el hilo del `Timer` (`Timer-0`)  
   Un `java.util.Timer` ejecuta sus tareas en su propio hilo de fondo, que aquí se llama `Timer-0`. Es un callback diferido, como en la clase 1: corre después y en otro hilo.
23. `cancel`  
   `cancel()`: si la tarea aún no se ha ejecutado, ya no se ejecutará. Es la baja del callback diferido.
24. d) En los dos, quien da de alta no guarda lo que necesita para dar de baja (la lambda / la tarea)  
   Sin la referencia (lambda o `TimerTask`) no hay forma de dar de baja. La regla común: quien da de alta guarda lo que necesita para dar de baja.
25. c) Escena 4 · La cocina  
   Solo la escena 4 termina con una traza. Las otras cuatro acaban «bien»: sus fallos solo los detecta quien predijo la salida antes de ejecutar.
26. d) Fila 8 · preguntas 2 y 4  
   Fila 8 (callback diferido sin cancelar) y preguntas 2 (¿síncrono o diferido?) y 4 (¿quién da de baja?).
27. `baja`  
   Para dar de **baja**: la lambda en la escena 3, la `TimerTask` en la escena 5.
28. c) Quien publicó primero: se queda en el `while` hasta entregarlo todo  
   Las llamadas reentrantes solo encolan y retornan. La primera es la que recorre el `while`. La pregunta para la U3 es qué pasa si ni siquiera ella se queda: dejar el aviso e irse lleva al bucle de eventos.
29. 1. Añade el aviso al final de `pendientes` → 2. Comprueba `notificando` y, como es `false`, sigue → 3. Pone `notificando = true` → 4. Saca el primer aviso de la cola → 5. Lo entrega a la foto de suscriptores → 6. Con la cola vacía, pone `notificando = false` en el `finally`  
   Encolar → comprobar → marcar → sacar y entregar (en bucle mientras queden avisos) → desmarcar en el `finally`, que se ejecuta aunque un suscriptor lance.

---

# Tema 3 · Bucle de eventos, colas y dispatching

Del publicar que llama en el acto a una cola en medio: productor, cola, bucle y handler; FIFO y prioridades con el panel de una caldera; run-to-completion, coalescencia, colas llenas, handlers largos y el Looper de Android.

## De la llamada directa a la cola

_La barra del bar en Kotlin (productor, cola, bucle y handler), run-to-completion y la ficha de traza del panel de una caldera recorrida con cola FIFO y con cola de prioridad._

### ¿Y si llegan tres eventos mientras el programa está ocupado? · Teoría

Hasta ahora, **quien producía un evento esperaba a que se atendiera**. Repasa cómo lo hemos ido viendo:

<div class="cards">
  <div class="card"><h4>U1 · Fuente, evento, receptor</h4><p>Un <code>while</code> que obtiene un evento y lo entrega. Solo tenía uno en la mano: para el siguiente había que esperar al teclado.</p></div>
  <div class="card"><h4>U2 · Callbacks y despacho</h4><p><code>publicar</code> llamaba en el acto a los handlers. Quien publicaba esperaba hasta que el último terminaba.</p></div>
  <div class="card"><h4>U3 · Una cola en medio</h4><p>Quien produce no espera: deja el evento y se va. Alguien lo atiende después, cuando le toque.</p></div>
</div>

### Lo que dejamos pendiente en la U2

En la clase 5 de la U2 arreglamos la reentrancia de la central de pedidos cambiando una sola palabra: `CentralConCola`. Su `publicar` ya llevaba dentro una cola y un `while`:

```kotlin
fun publicar(estado: String) {
    pendientes.addLast(estado)
    if (notificando) return
    notificando = true
    try {
        while (pendientes.isNotEmpty()) {
            val siguiente = pendientes.removeFirst()
            for (o in oyentes.toList()) {
                o(siguiente)
            }
        }
    } finally {
        notificando = false
    }
}
```

- `pendientes` guarda los avisos que llegan **durante** una ronda.
- La vacía **quien publicó primero**: se queda dentro hasta entregarlo todo.
- El `while` entrega los avisos de uno en uno: cada ronda termina antes de que empiece la siguiente.

La pregunta con la que cerramos la U2 era: *¿y si quien publica dejara el aviso y se fuera?* Esa es exactamente la idea de esta unidad.

> [!ANALOGIA] La barra del bar
> ¿Has esperado alguna vez en una barra donde el camarero no te toma nota hasta que termina de preparar lo del anterior? Eso es la llamada directa. Hoy ponemos una fila de comandas en medio.

**Preguntas**

**1.** En la U2, con un `publicar` que llama a los handlers en el acto, ¿cuándo puede seguir quien publica?

   a) En cuanto deja el evento en la cola
   b) Cuando el último handler ha terminado
   c) Cuando el primer handler ha empezado
   d) Nunca: publicar no devuelve el control

**2.** En `CentralConCola`, ¿quién vacía la cola `pendientes`?

   a) Un hilo aparte que la revisa cada cierto tiempo
   b) Cada suscriptor, cuando termina su aviso
   c) Quien publicó primero: se queda en el `while` hasta entregarlo todo
   d) Nadie: los avisos que llegan durante una ronda se descartan

### La barra del bar: cuatro piezas · Teoría

Una cola de eventos mínima tiene **cuatro papeles**. En la barra de un bar se ven muy bien:

<div class="flow">
  <div><b>Productor</b><small>quien pide</small></div>
  <div><b>Cola de eventos</b><small>la fila de comandas: entra por el final, sale por el principio</small></div>
  <div><b>Bucle</b><small>saca la primera y la entrega</small></div>
  <div><b>Handler</b><small>prepara UNA</small></div>
</div>

Quien pide **no espera** a que le preparen: deja la comanda y se va. Si en la cola hay `bocadillo`, `zumo` y `cafe` (el café llegó primero), lo siguiente que sale es el café, y el bocadillo, que entró el último, saldrá el último.

### Tres reglas

| Regla | En el bar |
| --- | --- |
| **1 · FIFO** | La primera comanda que entró es la primera que sale. |
| **2 · De una en una** | El camarero termina una comanda entera antes de coger la siguiente. |
| **3 · Se anota, no se prepara** | Si al preparar hace falta algo más, se anota al final de la fila; no se prepara en el acto. |

### Del bar al programa

Los mismos cuatro papeles, con otros nombres:

| En el bar | En el programa | En `Comandas.kt` |
| --- | --- | --- |
| Quien pide | Productor | `pedir(comanda)` |
| La fila de comandas | Cola de eventos | `comandas` (`ArrayDeque`) |
| El camarero que coge la primera y vuelve a mirar | Bucle de eventos | `atenderBarra()` |
| Preparar una comanda | Handler | `preparar(comanda)` |

> [!IMPORTANTE]
> El bucle no sabe qué es un café: **saca y entrega**. Todo lo que sabe hacer el bar está en el handler.

> [!EXAMEN]
> Saber poner nombre a las cuatro piezas en un ejemplo nuevo (productor, cola, bucle, handler) y enunciar las tres reglas es pregunta típica.

**Preguntas**

**3.** Ordena el recorrido de una comanda, desde que se pide hasta que se prepara.

   Ordena: El bucle la saca cuando es la primera · El handler la prepara · El productor (quien pide) deja la comanda · La comanda espera en la cola

**4.** En `Comandas.kt`, ¿qué función hace el papel de **bucle de eventos**? (solo el nombre)

**5.** Preparando un desayuno, el camarero ve que necesita un café. Según la regla 3, ¿qué hace?

   a) Prepara el café en el acto y luego sigue con el desayuno
   b) Anota el café al final de la fila
   c) Anota el café al principio de la fila para que salga el siguiente
   d) Deja el desayuno a medias y atiende la siguiente comanda

### PASO 1 · Llamada directa: pedir es preparar · Práctica

En clase lo tecleamos en vivo en **pde-consola**, en un fichero `Comandas.kt`. Empezamos por el punto de partida, como en la U2: quien pide llama directamente al handler.

```kotlin
fun preparar(comanda: String) {
    when (comanda) {
        else -> println("  preparado: $comanda")
    }
}

fun main() {
    println("PASO 1 - llamada directa")
    preparar("cafe")
    preparar("zumo")
}
```

Salida:

```text
PASO 1 - llamada directa
  preparado: cafe
  preparado: zumo
```

Dos cosas que hay que ver aquí:

1. **Pedir es preparar.** Quien llama a `preparar` espera a que termine. No hay forma de "dejar la comanda e irse".
2. **Igual que en la U2.** `publicar` llamaba al handler en el acto; aquí `main` llama a `preparar` en el acto.

El `when` con un solo `else` parece raro, pero está preparado para crecer: en el PASO 3 le añadiremos un caso especial.

> [!CUIDADO]
> Si el café tarda dos minutos, quien pide el zumo espera dos minutos. Con llamada directa, el ritmo lo marca el handler más lento, y quien produce queda bloqueado mientras tanto.

Si tienes pde-consola a mano, crea `Comandas.kt` con este código y ejecútalo: en los dos pasos siguientes lo iremos ampliando sin borrar nada.

**Preguntas**

**6.** Con este PASO 1, si preparar el café tardase dos minutos, ¿qué le pasaría a quien pide el zumo?

   a) Nada: el zumo se prepara a la vez que el café
   b) Esperaría esos dos minutos antes de poder pedir
   c) El zumo se perdería
   d) El zumo saldría antes porque es más rápido de preparar

**7.** En el PASO 1, ¿qué piezas de las cuatro **faltan**?

   a) El productor y el handler
   b) La cola y el bucle
   c) Solo el handler
   d) No falta ninguna

### PASO 2 · Una cola y un bucle · Práctica

Encima de lo anterior añadimos las tres piezas que faltaban: **la cola, quien anota y quien atiende**.

```kotlin
val comandas = ArrayDeque<String>()

fun pedir(comanda: String) {
    comandas.addLast(comanda)
    println("  anotado: $comanda (en cola: ${comandas.size})")
}

fun atenderBarra() {
    while (comandas.isNotEmpty()) {
        val comanda = comandas.removeFirst()
        preparar(comanda)
    }
    println("  (cola vacia: el bucle se queda esperando)")
}

fun main() {
    println("PASO 2 - cola y bucle")
    pedir("cafe")
    pedir("zumo")
    pedir("bocadillo")
    atenderBarra()
}
```

- **La cola** (`comandas`): lo pedido y todavía no preparado.
- **Entra por el final** (`addLast`): quien pide anota y se va.
- **Sale por el principio** (`removeFirst`): la más antigua primero. Eso es **FIFO**.

`atenderBarra` no decide nada: mientras haya comandas, saca la más antigua y se la entrega al handler.

Salida:

```text
PASO 2 - cola y bucle
  anotado: cafe (en cola: 1)
  anotado: zumo (en cola: 2)
  anotado: bocadillo (en cola: 3)
  preparado: cafe
  preparado: zumo
  preparado: bocadillo
  (cola vacia: el bucle se queda esperando)
```

1. **Primero se anota todo**: las tres líneas de «anotado» salen antes que ningún «preparado». Nadie ha esperado.
2. **Después, en orden de llegada**: se prepara en el mismo orden en que se pidió.
3. **Y el bucle se queda sin trabajo**: aquí termina porque es un programa de consola.

> [!NOTA]
> Un bucle de verdad (el de Android, el de un navegador, el de un firmware) **no termina**: se queda dormido hasta que llega algo.

**Preguntas**

**8.** ¿Qué llamada sobre el `ArrayDeque` hace que salga siempre la comanda más antigua? (solo el nombre del método)

**9.** Si en `atenderBarra` cambiases `removeFirst()` por `removeLast()`, ¿en qué orden saldrían los «preparado»?

   a) cafe, zumo, bocadillo
   b) bocadillo, zumo, cafe
   c) zumo, cafe, bocadillo
   d) Solo saldría bocadillo

### PASO 3 · Un handler que encola (predice antes de ejecutar) · Ejercicio

Un **desayuno** es un café y una tostada. Cambiamos el handler para que, al recibir un desayuno, **pida** las dos cosas:

```kotlin
fun preparar(comanda: String) {
    when (comanda) {
        "desayuno" -> {
            pedir("cafe")
            pedir("tostada")
            println("  desayuno: repartido en dos comandas")
        }
        else -> println("  preparado: $comanda")
    }
}

// en main
pedir("desayuno")
pedir("zumo")
atenderBarra()
```

> [!IMPORTANTE]
> Fíjate: el handler del desayuno llama a `pedir`, **no** a `preparar`. No prepara el café: lo anota.

### Antes de ejecutar

Hay un zumo esperando detrás del desayuno. En clase se votó a mano alzada: **¿qué sale antes, el café del desayuno o el zumo?**

Recorre la cola a mano. Te ayuda apuntar en un papel cómo queda `comandas` después de cada línea:

| Momento | Cola después |
| --- | --- |
| `pedir("desayuno")` | `[desayuno]` |
| `pedir("zumo")` | `[desayuno, zumo]` |
| el bucle saca el desayuno | ? |
| el handler pide café y tostada | ? |
| ... | ... |

Pista para el número «en cola»: cuando el handler del desayuno empieza, el desayuno **ya ha salido** de la cola.

No sigas hasta tener tu predicción completa: en el paso siguiente verás la salida real.

**Preguntas**

**10.** ¿Qué se prepara antes?

   a) El café del desayuno
   b) El zumo
   c) Salen a la vez
   d) Depende de cuál tarde menos

**11.** Ordena las líneas que imprime el PASO 3 (sin la cabecera).

   Ordena: (cola vacia: el bucle se queda esperando) · anotado: cafe (en cola: 2) · anotado: desayuno (en cola: 1) · anotado: tostada (en cola: 3) · anotado: zumo (en cola: 2) · desayuno: repartido en dos comandas · preparado: cafe · preparado: tostada · preparado: zumo

### Sale antes el zumo: run-to-completion · Teoría

Esta es la salida real del PASO 3:

```text
PASO 3 - un handler que encola
  anotado: desayuno (en cola: 1)
  anotado: zumo (en cola: 2)
  anotado: cafe (en cola: 2)
  anotado: tostada (en cola: 3)
  desayuno: repartido en dos comandas
  preparado: zumo
  preparado: cafe
  preparado: tostada
  (cola vacia: el bucle se queda esperando)
```

1. **El café y la tostada van al final.** Se anotan detrás del zumo, que ya estaba en la cola.
2. **El handler termina entero.** «desayuno: repartido…» sale antes que cualquier «preparado»: solo cuando el handler del desayuno acaba, el bucle coge la comanda siguiente.
3. **Con llamada directa, al revés.** Si el handler llamase a `preparar("cafe")`, el café se habría preparado *dentro* del desayuno, antes que el zumo.

> [!IMPORTANTE] Run-to-completion
> Un handler **nunca es interrumpido por otro**. Lo que produce mientras se ejecuta espera su turno en la cola.

Esto es lo que diferencia de verdad la cola de la llamada directa: con cola, cada evento se atiende **de principio a fin** y los eventos nuevos **no se cuelan**. El orden de ejecución pasa a depender solo del orden en que las cosas entran en la cola.

![Dos filas comparan el handler del desayuno. Arriba, con llamada directa: dentro de la caja preparar(desayuno) se ejecutan preparar(cafe) y preparar(tostada), y el zumo va después. Abajo, con cola y bucle: preparar(desayuno) termina entero tras anotar café y tostada, luego se prepara el zumo y después el café y la tostada, que se anotaron al final de la cola](/img/pde/tema-3/llamada-vs-cola.svg "Llamada directa: el café se prepara dentro del desayuno. Con cola: se anota al final y espera detrás del zumo")

<div class="cards">
  <div class="card"><h4>Llamada directa</h4><p>Un handler dentro de otro: el café se prepara mientras el desayuno sigue a medias.</p></div>
  <div class="card"><h4>Cola + bucle</h4><p>El desayuno termina; el café espera detrás del zumo.</p></div>
</div>

> [!EXAMEN]
> «¿Qué imprime?» con un handler que encola es un clásico. La clave: lo encolado por un handler va **detrás de todo lo que ya esperaba** y no empieza hasta que ese handler termina.

**Preguntas**

**12.** Si el handler del desayuno llamase a `preparar("cafe")` en lugar de `pedir("cafe")`, ¿qué se imprimiría antes?

   a) «preparado: zumo»
   b) «preparado: cafe»
   c) «desayuno: repartido en dos comandas»
   d) «anotado: cafe»

**13.** Marca las afirmaciones correctas sobre run-to-completion.

   a) Un handler se ejecuta entero antes de que el bucle saque el siguiente evento
   b) Un evento encolado por un handler puede interrumpirlo si es urgente
   c) Lo que produce un handler espera su turno en la cola
   d) Garantiza que todos los eventos se atienden en el mismo instante en que ocurren

### El panel de una caldera (Anexo 0) · Teoría

Para la Actividad 1 cambiamos de escenario: el **panel de una caldera** recibe tres tipos de evento. Cada uno tiene una notación corta (la que usaremos en la ficha), una prioridad y la línea que muestra el handler al atenderlo.

| Evento | Notación | Prioridad | Línea que muestra el handler |
| --- | --- | --- | --- |
| `Pulsacion(boton)` | `P:SUBIR` | NORMAL | Pulsado SUBIR |
| `Lectura(grados)` | `L21` | BAJA | Lectura 21 grados |
| `Emergencia(motivo)` | `E:humo` | ALTA | EMERGENCIA: humo |

`L21` es una lectura de 21 grados, `P:SUBIR` una pulsación del botón SUBIR y `E:humo` una emergencia por humo. En la ficha, **el primero de la lista es el siguiente en salir**.

> [!IMPORTANTE] Regla del handler
> Al atender una lectura de **30 grados o más**, el handler **publica** una `Emergencia("temperatura N")`. No la atiende él: la publica. (En la ficha se puede abreviar `E:t31`.)

El handler, tal como está en `PanelEventos.kt`:

```kotlin
fun manejarPanel(evento: EventoPanel, publicar: (EventoPanel) -> Unit): String =
    when (evento) {
        is Pulsacion -> "Pulsado ${evento.boton}"
        is Lectura -> {
            if (evento.grados >= 30) publicar(Emergencia("temperatura ${evento.grados}"))
            "Lectura ${evento.grados} grados"
        }
        is Emergencia -> "EMERGENCIA: ${evento.motivo}"
    }
```

Recibe un evento y **devuelve** la línea que muestra el panel. El parámetro `publicar` es una **función**: es la manera que tiene el handler de producir un evento nuevo sin atenderlo él.

Y las prioridades:

```kotlin
enum class PrioridadPanel { ALTA, NORMAL, BAJA } // cuanto más arriba, antes se atiende

fun prioridadDelEvento(evento: EventoPanel): PrioridadPanel =
    when (evento) {
        is Emergencia -> PrioridadPanel.ALTA
        is Pulsacion -> PrioridadPanel.NORMAL
        is Lectura -> PrioridadPanel.BAJA
    }
```

**Preguntas**

**14.** ¿Qué línea devuelve `manejarPanel` al atender `L31`?

**15.** Según `prioridadDelEvento`, ¿qué prioridad tiene una pulsación?

   a) ALTA
   b) NORMAL
   c) BAJA
   d) Depende del botón

**16.** ¿Qué es el parámetro `publicar` de `manejarPanel`?

   a) Un booleano que indica si hay que imprimir la línea
   b) Una función que el handler llama para producir un evento nuevo
   c) La cola de eventos
   d) La prioridad del evento

### Sin cola, la emergencia sale antes que su lectura · Ejercicio

Primero lo vemos sin cola. En `PanelDirecto.kt` cada evento se entrega al handler **en el acto**: quien produce el evento llama al handler y espera. Con los eventos del guion de la actividad, la salida es:

```text
Panel con llamada directa (sin cola)
  Lectura 21 grados
  Pulsado SUBIR
  EMERGENCIA: temperatura 31
  Lectura 31 grados
  Pulsado MODO
  EMERGENCIA: humo
```

Tres cosas que observar:

1. **Entrega en el acto.** Los eventos salen exactamente en el orden en que se producen; no hay nada que reordenar.
2. **La emergencia sale antes.** «EMERGENCIA: temperatura 31» aparece **antes** que la «Lectura 31 grados» que la provoca.
3. **Un handler dentro de otro.** El handler de la emergencia se ejecuta *dentro* del de la lectura: `manejarPanel(L31)` llama a `publicar`, y con llamada directa `publicar` atiende la emergencia en el acto. La línea de la emergencia sale y, cuando vuelve, la lectura termina y devuelve su propia línea.

Es el mismo fenómeno que la reentrancia de la U2: el orden de la salida no coincide con el orden lógico de los hechos (primero se lee 31 grados, luego hay emergencia).

> [!NOTA]
> Ahora pondrás una cola en medio, en papel. ¿Dónde saldrá esa emergencia?

**Preguntas**

**17.** ¿Por qué en `PanelDirecto.kt` «EMERGENCIA: temperatura 31» sale antes que «Lectura 31 grados»?

   a) Porque las emergencias tienen prioridad ALTA
   b) Porque `publicar` atiende la emergencia en el acto, antes de que el handler de la lectura devuelva su línea
   c) Porque el sensor envía la emergencia antes que la lectura
   d) Porque la cola FIFO la coloca delante

**18.** En el momento en que se imprime «EMERGENCIA: temperatura 31», ¿cuántas ejecuciones de `manejarPanel` hay en marcha a la vez? (un número)

### Actividad 1 · Parte A: cola FIFO · Ejercicio

En clase, en pareja y en papel: **9 minutos con la cola FIFO** y 8 con la de prioridad (Anexo 1, ficha de traza).

### El guion: 11 pasos

| Paso | Operación |
| --- | --- |
| 1 a 3 | encolar `L21`, `P:SUBIR`, `L31` |
| 4 | atender uno |
| 5 y 6 | encolar `P:MODO`, `E:humo` |
| 7 a 11 | atender uno, hasta vaciar la cola |

En cada fila escribe **cómo queda la cola después del paso** (el primero de la lista es el siguiente en salir) y, si se atiende un evento, **qué línea sale**. En «atender uno» se atiende **uno solo**. No olvides la regla del handler: una lectura de 30 grados o más publica una emergencia (`E:t31`).

**Parte A · Cola FIFO**: se atiende en orden de llegada.

| Paso | Operación | Cola después | Línea que sale |
| --- | --- | --- | --- |
| 1 | encolar L21 | `[L21]` | |
| 2 | encolar P:SUBIR | | |
| 3 | encolar L31 | | |
| 4 | atender uno | | |
| 5 | encolar P:MODO | | |
| 6 | encolar E:humo | | |
| 7–11 | atender uno | | |

Rellénala a mano y **después** compruébala con el simulador (modo FIFO, paso a paso):

> _Aquí hay un simulador interactivo (`cola-panel`): ábrelo en la web._
> [!TRUCO]
> El evento que publica un handler entra en la cola **en el paso en que se atiende su lectura**, y va al final.

**Preguntas**

**19.** Con la cola FIFO, ordena los eventos según se atienden.

   Ordena: E:humo · E:t31 · L21 · L31 · P:MODO · P:SUBIR

**20.** **A1.** ¿En qué puesto se atiende `E:humo` con la cola FIFO? (un número)

**21.** **A1 (bis).** Cuando `E:humo` llega a la cola (paso 6), ¿cuántos eventos tiene delante?

### Parte A resuelta y preguntas A2 y A3 · Ejercicio

Compara tu ficha con la solución de la Parte A:

| Paso | Operación | Cola después | Línea que sale |
| --- | --- | --- | --- |
| 1 | encolar L21 | `[L21]` | — |
| 2 | encolar P:SUBIR | `[L21, P:SUBIR]` | — |
| 3 | encolar L31 | `[L21, P:SUBIR, L31]` | — |
| 4 | atender uno | `[P:SUBIR, L31]` | Lectura 21 grados |
| 5 | encolar P:MODO | `[P:SUBIR, L31, P:MODO]` | — |
| 6 | encolar E:humo | `[P:SUBIR, L31, P:MODO, E:humo]` | — |
| 7 | atender uno | `[L31, P:MODO, E:humo]` | Pulsado SUBIR |
| 8 | atender uno | `[P:MODO, E:humo, E:t31]` | Lectura 31 grados |
| 9 | atender uno | `[E:humo, E:t31]` | Pulsado MODO |
| 10 | atender uno | `[E:t31]` | EMERGENCIA: humo |
| 11 | atender uno | `[]` | EMERGENCIA: temperatura 31 |

Fíjate en la fila 8: al atender `L31`, la cola **pierde** `L31` y **gana** `E:t31` al final. La línea que sale es la de la lectura; la emergencia tendrá que esperar a que salgan los que ya estaban (`P:MODO` y `E:humo`).

> [!CUIDADO] Errores frecuentes en la ficha
> - Atender dos eventos en un mismo «atender uno».
> - Poner `E:t31` en el paso 3, cuando se encola `L31`. Encolar una lectura no ejecuta su handler: la emergencia nace al **atenderla**.
> - Poner `E:t31` al principio de la cola «porque es una emergencia». En FIFO no hay prioridades.

Compara también con `PanelDirecto.kt`: allí la emergencia de temperatura salía **antes** que su lectura; aquí sale **la última**, después de su lectura.

**Preguntas**

**22.** **A2.** ¿En qué paso entra en la cola `E:temperatura 31` y quién la encola?

   a) En el paso 3, la encola quien encola `L31`
   b) En el paso 8, la encola el handler de `L31` (a través de `publicar`)
   c) En el paso 8, la encola el bucle porque detecta los 31 grados
   d) En el paso 11, la encola el handler de `E:humo`

**23.** **A3.** En `PanelDirecto.kt` esa emergencia salía antes que su lectura. ¿Por qué con cola sale después?

   a) Porque la cola FIFO da menos prioridad a las emergencias
   b) Porque el handler de la lectura publica (encola) la emergencia y termina; la emergencia espera su turno en la cola
   c) Porque la cola invierte el orden de los eventos
   d) Porque con cola el handler ya no publica la emergencia

### Actividad 1 · Parte B: cola con prioridad · Ejercicio

Mismo guion, mismos eventos, mismo bucle y mismo handler. Lo único que cambia es **la cola**.

**Parte B · Cola con prioridad**: **ALTA** antes que **NORMAL**, NORMAL antes que **BAJA**. **Entre iguales, orden de llegada.**

| Prioridad | Eventos |
| --- | --- |
| ALTA | `E:humo`, `E:t31` |
| NORMAL | `P:SUBIR`, `P:MODO` |
| BAJA | `L21`, `L31` |

Cómo se trabaja en la ficha: al encolar, el evento **se coloca delante del primero que sea menos urgente que él**; si hay otros de su misma prioridad, se queda detrás de ellos. «Atender uno» saca siempre el primero de la lista.

| Paso | Operación | Cola después | Línea que sale |
| --- | --- | --- | --- |
| 1 | encolar L21 | `[L21]` | |
| 2 | encolar P:SUBIR | | |
| 3 | encolar L31 | | |
| 4 | atender uno | | |
| 5 | encolar P:MODO | | |
| 6 | encolar E:humo | | |
| 7–11 | atender uno | | |

Rellena la columna y comprueba con el simulador, ahora en modo prioridad:

> _Aquí hay un simulador interactivo (`cola-panel`): ábrelo en la web._
> [!TRUCO]
> En el paso 4 ya se nota la diferencia: hay una lectura que llegó antes, pero ¿es la más urgente?

**Preguntas**

**24.** Con la cola con prioridad, ordena los eventos según se atienden.

   Ordena: E:humo · E:t31 · L21 · L31 · P:MODO · P:SUBIR

**25.** **B1.** ¿En qué puesto se atiende ahora `E:humo`? (un número)

### Parte B resuelta y preguntas B2 a B4 · Ejercicio

Solución de la Parte B:

| Paso | Operación | Cola después | Línea que sale |
| --- | --- | --- | --- |
| 1 | encolar L21 | `[L21]` | — |
| 2 | encolar P:SUBIR | `[P:SUBIR, L21]` | — |
| 3 | encolar L31 | `[P:SUBIR, L21, L31]` | — |
| 4 | atender uno | `[L21, L31]` | Pulsado SUBIR |
| 5 | encolar P:MODO | `[P:MODO, L21, L31]` | — |
| 6 | encolar E:humo | `[E:humo, P:MODO, L21, L31]` | — |
| 7 | atender uno | `[P:MODO, L21, L31]` | EMERGENCIA: humo |
| 8 | atender uno | `[L21, L31]` | Pulsado MODO |
| 9 | atender uno | `[L31]` | Lectura 21 grados |
| 10 | atender uno | `[E:t31]` | Lectura 31 grados |
| 11 | atender uno | `[]` | EMERGENCIA: temperatura 31 |

### Mismos eventos, otra cola, otro orden

| Cola | Orden de atención |
| --- | --- |
| FIFO | L21, P:SUBIR, L31, P:MODO, E:humo, E:temperatura 31 |
| Prioridad | P:SUBIR, E:humo, P:MODO, L21, L31, E:temperatura 31 |

<div class="cards">
  <div class="card"><h4>E:humo</h4><p>Con FIFO, la quinta: esperó detrás de tres. Con prioridad, la segunda.</p></div>
  <div class="card"><h4>L21</h4><p>Alguien lo paga: llegó la primera y con prioridad se atiende la cuarta.</p></div>
  <div class="card"><h4>E:temperatura 31</h4><p>La última en las dos colas, siempre después de su lectura.</p></div>
</div>

> [!IMPORTANTE]
> El bucle y el handler no han cambiado. **El orden lo decide la cola, no el bucle.**

> [!EXAMEN]
> Prioridad **no es** interrupción: la emergencia adelanta en la cola, pero no corta al handler que se esté ejecutando.

**Preguntas**

**26.** **B2.** ¿Qué evento sale perdiendo respecto a la Parte A? (notación de la ficha)

**27.** **B3.** Cuando llega `E:humo`, ¿interrumpe al handler de algún otro evento?

   a) Sí, interrumpe al handler de P:SUBIR
   b) Sí, interrumpe al handler de cualquier evento de prioridad menor
   c) No: adelanta en la cola, pero ningún handler se interrumpe (run-to-completion)
   d) No, porque se descarta hasta que la cola se vacía

**28.** **B4.** Si no dejaran de llegar pulsaciones, ¿cuándo se atendería `L21`?

   a) Al cabo de 11 pasos, como en el guion
   b) En cuanto fuese la más antigua de la cola
   c) Nunca: siempre habría algo más urgente delante (inanición)
   d) Inmediatamente, porque las lecturas tienen la prioridad más alta

### Cierre: tres ideas y una pregunta para la Hora 2 · Teoría

### Tres ideas que te llevas

<div class="cards">
  <div class="card"><h4>1 · La cola separa</h4><p>...cuándo <b>ocurre</b> un evento de cuándo <b>se atiende</b>. Quien produce deja el evento y sigue.</p></div>
  <div class="card"><h4>2 · De uno en uno y hasta el final</h4><p>El bucle saca un evento, el handler lo termina entero (run-to-completion) y solo entonces se saca el siguiente.</p></div>
  <div class="card"><h4>3 · El orden lo decide la cola</h4><p>Mismo bucle y mismo handler: con FIFO o con prioridad, el orden de atención cambia.</p></div>
</div>

### La ficha es la especificación de la Hora 2

La ficha rellena es tuya: guárdala. En la Hora 2 recibes el programa completo y **imprime lo que hoy has escrito a mano**. Antes de la Hora 2:

- Copia en tu pde-consola los cuatro archivos de `TRAB_U3-1_alumno.zip`.
- Comprueba que `ColaPanel.kt` se ejecuta.

### Para pensar

El handler `manejarPanel` recibe un parámetro que se llama `publicar`. **¿Qué le pasa el bucle ahí?** Búscalo en `ColaPanel.kt`.

Piensa en lo que has visto hoy: en `PanelDirecto.kt`, publicar una emergencia equivalía a atenderla en el acto (un handler dentro de otro). En la ficha, en cambio, la emergencia **entraba en la cola** y esperaba. Lo que el handler recibe en `publicar` es lo que decide cuál de los dos comportamientos tienes, sin cambiar ni una línea del handler.

> [!NOTA]
> Es el mismo truco que la inversión de control de la U2: el handler no sabe a quién avisa; se lo inyecta quien lo llama.

**Preguntas**

**29.** Con cola, ¿qué tiene que pasarle el bucle a `manejarPanel` como `publicar` para que la emergencia salga después de su lectura?

   a) Una función que llama a `manejarPanel` con el evento nuevo
   b) Una función que mete el evento nuevo en la cola
   c) Una función que imprime el evento nuevo con `println`
   d) Una función vacía que ignora el evento

**30.** ¿Cuáles son las tres ideas de la sesión?

   a) La cola separa cuándo ocurre un evento de cuándo se atiende
   b) El bucle atiende de uno en uno y hasta el final
   c) Un evento urgente interrumpe al handler en curso
   d) El orden lo decide la cola
   e) El orden lo decide el bucle

### Repaso: de la llamada directa a la cola · Repaso

> [!RESUMEN] Chuleta
> - **Llamada directa**: producir = atender. Quien produce espera, y un handler puede ejecutarse dentro de otro.
> - **Cuatro piezas**: productor (`pedir`), cola (`ArrayDeque`), bucle (`atenderBarra`), handler (`preparar`).
> - **FIFO** = `addLast` + `removeFirst`.
> - **Run-to-completion**: el handler termina entero; lo que produce va a la cola y espera su turno.
> - **Handler que encola**: lo nuevo va **detrás** de todo lo que ya esperaba.
> - **Prioridad**: ALTA > NORMAL > BAJA; entre iguales, orden de llegada. Adelanta en la cola, **no interrumpe**.
> - **Inanición**: si lo urgente no deja de llegar, lo menos urgente no se atiende nunca.
> - **El orden lo decide la cola**, no el bucle.

| Situación | Dónde sale el evento publicado por un handler |
| --- | --- |
| Llamada directa | Dentro del handler que lo publica (antes que la línea de este) |
| Cola FIFO | Al final de la cola, después de todo lo que esperaba |
| Cola con prioridad | Delante de los menos urgentes, pero siempre después de que termine el handler actual |

Responde como si fuera el parcial.

**Preguntas**

**31.** Cola FIFO del panel con `[P:MODO, L35]`. Se atiende uno y llega `E:humo`. ¿Cómo queda la cola?

   a) `[E:humo, L35]`
   b) `[L35, E:humo]`
   c) `[L35, E:t35, E:humo]`
   d) `[E:humo, L35, E:t35]`

**32.** Cola con prioridad vacía. Se encolan, por este orden, `L40`, `P:SUBIR` y `E:humo`, y después se atienden los tres. ¿Qué evento se atiende el **último**?

**33.** Marca las afirmaciones correctas.

   a) Con cola, quien produce un evento no espera a que se atienda
   b) Con cola con prioridad, una emergencia corta el handler de una lectura en curso
   c) Si un handler encola un evento, ese evento no empieza hasta que el handler termina
   d) Cambiar FIFO por prioridad exige reescribir el handler

### Solucionario

1. b) Cuando el último handler ha terminado  
   Llamada directa = esperar. `publicar` recorre los handlers y solo devuelve el control cuando el último ha terminado. Dejar el evento y seguir es justo lo que aporta la cola.
2. c) Quien publicó primero: se queda en el `while` hasta entregarlo todo  
   El primero que publica pone `notificando = true` y entra en el `while`. Los que publican durante la ronda encolan y se van (`return`). En la U3 damos el paso siguiente: que **nadie** que publique se quede a vaciar la cola, sino un bucle dedicado.
3. 1. El productor (quien pide) deja la comanda → 2. La comanda espera en la cola → 3. El bucle la saca cuando es la primera → 4. El handler la prepara  
   Productor → cola → bucle → handler. El productor no espera a nadie: deja la comanda y se va.
4. `atenderBarra`  
   `atenderBarra()` es el bucle: mientras haya comandas, saca la más antigua y se la da a `preparar`, que es el handler. `pedir` es el productor y `comandas` la cola.
5. b) Anota el café al final de la fila  
   Lo que haga falta mientras se prepara va **al final** de la fila. Prepararlo en el acto sería volver a la llamada directa; dejar el desayuno a medias rompería la regla 2 (de una en una).
6. b) Esperaría esos dos minutos antes de poder pedir  
   Con llamada directa todo es secuencial y bloqueante: `preparar("zumo")` no empieza hasta que `preparar("cafe")` devuelve el control.
7. b) La cola y el bucle  
   Hay quien pide (`main`) y quien prepara (`preparar`), pero no hay **cola** donde dejar la comanda ni **bucle** que la saque. Es lo que añade el PASO 2.
8. `removeFirst`  
   `addLast` mete por el final y `removeFirst` saca por el principio: la combinación da FIFO. Si sacases con `removeLast` tendrías una pila (LIFO).
9. b) bocadillo, zumo, cafe  
   Con `removeLast` la estructura se comporta como una pila (LIFO): sale primero lo último que entró. El bucle seguiría igual; lo que cambia el orden es **la cola**, no el bucle.
10. b) El zumo  
   El café se anota **al final** de la cola, detrás del zumo, que ya estaba esperando. Así que se prepara antes el zumo.
11. 1. anotado: desayuno (en cola: 1) → 2. anotado: zumo (en cola: 2) → 3. anotado: cafe (en cola: 2) → 4. anotado: tostada (en cola: 3) → 5. desayuno: repartido en dos comandas → 6. preparado: zumo → 7. preparado: cafe → 8. preparado: tostada → 9. (cola vacia: el bucle se queda esperando)  
   El handler del desayuno se ejecuta **entero** (anota café, anota tostada, imprime su línea) y solo entonces el bucle coge la siguiente comanda, que es el zumo. Café y tostada quedaron detrás.
12. b) «preparado: cafe»  
   Con llamada directa el café se prepara dentro del desayuno: «preparado: cafe» sale antes que la línea del desayuno y antes que el zumo. Un handler dentro de otro.
13. a) Un handler se ejecuta entero antes de que el bucle saque el siguiente evento; c) Lo que produce un handler espera su turno en la cola  
   Nada interrumpe al handler que se está ejecutando, ni siquiera algo urgente; lo nuevo va a la cola. Y precisamente por eso los eventos **no** se atienden cuando ocurren, sino cuando les toca.
14. `Lectura 31 grados`  
   Devuelve «Lectura 31 grados». Además, como 31 ≥ 30, antes de devolverla llama a `publicar(Emergencia("temperatura 31"))`: produce la emergencia, pero no la atiende.
15. b) NORMAL  
   Emergencia → ALTA, Pulsacion → NORMAL, Lectura → BAJA. El botón no influye.
16. b) Una función que el handler llama para producir un evento nuevo  
   Su tipo es `(EventoPanel) -> Unit`: una función que recibe un evento. El handler no sabe qué hace esa función por dentro; eso lo decide quien le llama (el bucle).
17. b) Porque `publicar` atiende la emergencia en el acto, antes de que el handler de la lectura devuelva su línea  
   No hay cola ni prioridad que valga: `manejarPanel(L31)` llama a `publicar` **antes** de devolver «Lectura 31 grados», y esa llamada ejecuta entero el handler de la emergencia. Un handler dentro de otro.
18. `2`  
   Dos: la de la lectura (a medias, esperando a que `publicar` vuelva) y la de la emergencia. Con cola esto nunca pasa: run-to-completion garantiza que solo hay una en marcha.
19. 1. L21 → 2. P:SUBIR → 3. L31 → 4. P:MODO → 5. E:humo → 6. E:t31  
   FIFO: L21, P:SUBIR, L31, P:MODO, E:humo, E:t31. La emergencia de temperatura entra al final cuando se atiende L31 (paso 8), detrás de P:MODO y E:humo.
20. `5`  
   Se atiende la **quinta**, en el paso 10. Una emergencia, esperando detrás de pulsaciones y lecturas.
21. `3`  
   Tres: `P:SUBIR`, `L31` y `P:MODO`. La cola tras el paso 6 es `[P:SUBIR, L31, P:MODO, E:humo]`.
22. b) En el paso 8, la encola el handler de `L31` (a través de `publicar`)  
   Entra en el paso 8, cuando se **atiende** `L31`. La encola el handler de la lectura llamando a `publicar`. El bucle no sabe nada de grados: solo saca y entrega.
23. b) Porque el handler de la lectura publica (encola) la emergencia y termina; la emergencia espera su turno en la cola  
   Con cola, `publicar` no atiende: **encola**. El handler de `L31` termina entero y devuelve «Lectura 31 grados»; la emergencia espera en la cola y sale cuando le toca. Ya no hay un handler dentro de otro.
24. 1. P:SUBIR → 2. E:humo → 3. P:MODO → 4. L21 → 5. L31 → 6. E:t31  
   Prioridad: P:SUBIR (paso 4, la más urgente de las que había), E:humo, P:MODO, L21, L31 y E:t31. La emergencia de temperatura nace en el paso 10 al atender L31 y es lo único que queda.
25. `2`  
   La **segunda**: en el paso 4 ya se ha atendido P:SUBIR; cuando E:humo llega en el paso 6 se pone delante de todo y sale en el paso 7.
26. `L21`  
   `L21`: llegó la primera y con FIFO se atendía la primera; con prioridad baja al **cuarto** puesto porque todas las pulsaciones y emergencias le adelantan.
27. c) No: adelanta en la cola, pero ningún handler se interrumpe (run-to-completion)  
   La prioridad decide **qué sale de la cola**, no corta lo que se está ejecutando. Cada handler termina entero; E:humo simplemente se coloca la primera para la siguiente vez que el bucle saque un evento.
28. c) Nunca: siempre habría algo más urgente delante (inanición)  
   **Nunca**: con prioridad estricta, un flujo continuo de eventos más urgentes deja sin atender a los de menos prioridad. Se llama **inanición** (*starvation*) y en la sesión de obligatorios veremos cómo evitarla.
29. b) Una función que mete el evento nuevo en la cola  
   Para que se respete run-to-completion, `publicar` debe **encolar** el evento (en la misma cola del bucle). Si llamase al handler sería `PanelDirecto.kt`; si lo ignorase, la emergencia se perdería. Compruébalo en `ColaPanel.kt`.
30. a) La cola separa cuándo ocurre un evento de cuándo se atiende; b) El bucle atiende de uno en uno y hasta el final; d) El orden lo decide la cola  
   Separar ocurrencia y atención, run-to-completion y «el orden lo decide la cola». Ni lo urgente interrumpe, ni el bucle decide el orden: el bucle solo saca y entrega.
31. b) `[L35, E:humo]`  
   Sale `P:MODO` (Pulsado MODO), que no publica nada, y `E:humo` se pone al final: `[L35, E:humo]`. `E:t35` aparecerá cuando se **atienda** `L35`.
32. `L40`  
   Orden: `E:humo` (ALTA), `P:SUBIR` (NORMAL) y `L40` (BAJA). Al atender `L40` se publica `E:t40`, que quedaría en la cola para un cuarto «atender».
33. a) Con cola, quien produce un evento no espera a que se atienda; c) Si un handler encola un evento, ese evento no empieza hasta que el handler termina  
   La cola desacopla productor y handler, y run-to-completion hace que lo encolado espere. La prioridad no interrumpe, y en la actividad el handler y el bucle no cambiaron: solo la cola.

## Bucle de eventos, colas y dispatching

_Las piezas del event loop, por qué la cola absorbe ráfagas pero no sobrecargas, prioridad, coalescencia y descarte, run-to-completion y handlers largos, y cómo se ve todo dentro de Android (Looper, Handler, MessageQueue)._

### La idea de la unidad: la cocina del restaurante · Teoría

La sesión responde a tres preguntas: **de dónde sale la secuencia de eventos, cómo se despachan y qué pasa si un handler tarda**.

| Bloque | Qué se ve |
| --- | --- |
| 1 · Productores, cola y bucle | Las piezas del sistema; orden, prioridad, fusión y descarte. |
| 2 · Run-to-completion | Cada evento termina antes del siguiente; consecuencias de un handler largo. |
| 3 · Android | Looper, Handler y MessageQueue; el presupuesto de 16 ms por fotograma. |

> [!IMPORTANTE] Idea de la unidad
> El handler **no** se ejecuta «cuando ocurre algo»: se ejecuta **cuando le toca**, y eso lo decide la cola.

### La cocina del restaurante

Varios camareros dejan comandas en el **pasador** y **un solo cocinero** las prepara de una en una. Llega la comanda de la mesa 3 y después la de la mesa 7: el cocinero coge la primera, cocina la mesa 3 **entera** (la 7 espera) y luego coge la siguiente.

<div class="cards">
  <div class="card"><h4>Camareros</h4><p>Los <b>productores</b>: toman nota y vuelven a su mesa, sin esperar.</p></div>
  <div class="card"><h4>Pasador</h4><p>La <b>cola</b>: guarda las comandas por orden de llegada.</p></div>
  <div class="card"><h4>Cocinero</h4><p>El <b>bucle</b>: coge una, la termina y coge la siguiente.</p></div>
  <div class="card"><h4>Receta</h4><p>El <b>handler</b>: lo que se hace con cada tipo de comanda.</p></div>
</div>

Quien produce y quien atiende **no se esperan**: el pasador absorbe la diferencia de ritmo entre ellos.

> [!NOTA]
> No es un ejemplo de juguete: es la arquitectura de **Android**, de un **navegador**, de **Node.js** y de un **firmware**. Cambian los nombres, no las piezas.

**Preguntas**

**1.** Según la idea de la unidad, ¿cuándo se ejecuta un handler?

   a) En el instante exacto en que ocurre el evento
   b) Cuando le toca, según lo decida la cola
   c) Cuando el productor termina de producir todos sus eventos
   d) Cuando el handler anterior lo llama

**2.** En la analogía de la cocina, ¿qué pieza del event loop es la **receta**?

   a) El productor
   b) La cola
   c) El bucle
   d) El handler

### Las piezas del sistema · Teoría

Los **productores** generan eventos, la **cola** los guarda, el **bucle** los saca y el **dispatcher** los entrega a su handler.

![Tres productores (pulsador con su ISR, mensaje de red y temporizador) dejan eventos en una cola FIFO con prioridad. Un bucle con dispatcher, recorrido por un único hilo, saca cada evento y consulta una tabla tipo → handler: PULSADOR → alPulsar, MENSAJE → alMensaje, TICK → alTick](/img/pde/tema-3/piezas-bucle.svg "Productores → cola → bucle + dispatcher → handlers")

| Pieza | Responsabilidad | Regla |
| --- | --- | --- |
| **Evento / mensaje** | Tipo + datos propios de ese tipo (+ instante en que ocurrió) | Pequeño e inmutable: es un **aviso**, no un trabajo |
| **Productor** | Detecta algo y encola un evento | **Rápido**: no ejecuta la reacción |
| **Cola** | Guarda los eventos hasta que les toca | Define **orden, prioridad y qué se descarta** |
| **Bucle + dispatcher** | Saca el siguiente evento y elige el handler por su tipo | **Un único hilo** lo recorre |

Algunas consecuencias que conviene tener claras:

- Una **ISR** (rutina de interrupción) de un pulsador es un productor típico: debe ser cortísima, así que solo encola el evento y sale. La reacción la hará después el handler, fuera de la interrupción.
- El evento lleva **datos**, no **trabajo**: «pulsador 4», «sensor 24 C». Lo que hay que hacer con él lo sabe el handler.
- Como un único hilo recorre el bucle, **los handlers nunca se ejecutan a la vez** entre sí.

> [!EXAMEN]
> La tabla pieza → responsabilidad → regla es material de pregunta corta: «¿qué no debe hacer un productor?» (ejecutar la reacción), «¿qué decide la cola?» (orden, prioridad y descarte).

**Preguntas**

**3.** Marca las reglas correctas de las piezas del event loop.

   a) El productor debe ser rápido y no ejecutar la reacción
   b) El evento debe llevar dentro el trabajo que hay que hacer
   c) La cola define orden, prioridad y qué se descarta
   d) Varios hilos recorren el bucle para ir más rápido

**4.** ¿Con qué criterio elige el dispatcher el handler de un evento?

   a) Por su prioridad
   b) Por su tipo
   c) Por el instante en que ocurrió
   d) Por el productor que lo generó

### Sensor sin cola: ¿cuántos se pierden? · Ejercicio

Un sensor de puerta genera **un evento cada 10 ms** y el handler **tarda 25 ms**. No hay cola: si el handler está ocupado cuando llega un evento, no hay sitio donde esperar.

```js
const llegadas = [0, 10, 20, 30, 40]; // ms: un evento cada 10 ms
const duracion = 25;                  // ms que tarda el handler
let libreEn = 0;

for (const t of llegadas) {
  if (t < libreEn) console.log(`t=${t}: PERDIDO (handler ocupado)`);
  else { console.log(`t=${t}: atendido`); libreEn = t + duracion; }
}
```

`libreEn` es el instante en que el handler vuelve a estar libre. Solo se atiende el evento que llega con el handler libre; al atenderlo, el handler queda ocupado otros 25 ms.

Haz la traza tú: para cada `t`, compara con `libreEn` y actualízalo si se atiende.

| t | ¿t < libreEn? | Resultado | libreEn después |
| --- | --- | --- | --- |
| 0 | 0 < 0: no | atendido | 25 |
| 10 | ? | ? | ? |
| 20 | ? | ? | ? |
| 30 | ? | ? | ? |
| 40 | ? | ? | ? |

> [!CUIDADO]
> Fíjate en que el código **no lanza ningún error** ni avisa de nada: lo que se pierde, desaparece en silencio.

**Preguntas**

**5.** ¿Qué eventos se pierden?

   a) t=0
   b) t=10
   c) t=20
   d) t=30
   e) t=40

**6.** ¿Qué pondrías entre el sensor y el handler para no perder eventos, y con qué orden de salida?

   a) Una pila, sacando el último que llegó
   b) Una cola, sacando por orden de llegada (FIFO)
   c) Una variable que guarde solo el último evento
   d) Nada: basta con hacer el handler más rápido que 10 ms

### Una cola entre medias: la pérdida se convierte en espera · Ejercicio

La salida del sensor sin cola fue:

```text
t=0: atendido
t=10: PERDIDO (handler ocupado)
t=20: PERDIDO (handler ocupado)
t=30: atendido
t=40: PERDIDO (handler ocupado)
```

Ahora ponemos una cola entre medias:

```js
const llegadas = [0, 10, 20, 30, 40];
const duracion = 25;
const cola = [...llegadas];
let reloj = 0;

while (cola.length) {
  const t = cola.shift();
  reloj = Math.max(reloj, t);
  console.log(`evento t=${t}: atendido en ${reloj} (espera ${reloj - t} ms)`);
  reloj += duracion;
}
```

```text
evento t=0: atendido en 0 (espera 0 ms)
evento t=10: atendido en 25 (espera 15 ms)
evento t=20: atendido en 50 (espera 30 ms)
evento t=30: atendido en 75 (espera 45 ms)
evento t=40: atendido en 100 (espera 60 ms)
```

1. **Los eventos esperan su turno** en vez de perderse.
2. **Se sacan por orden de llegada**, uno cada vez (`shift` saca el primero).
3. **El precio es la espera**: crece 15 ms por evento, porque el handler tarda 25 ms y el productor genera uno cada 10 ms.

> [!IMPORTANTE]
> La cola **absorbe las ráfagas**, pero no arregla un handler más lento que los productores: la espera crece (0, 15, 30, 45, 60 ms…) y la cola no deja de crecer.

Juega con el periodo del productor y la duración del handler y observa perdidos (sin cola) y esperas (con cola):

> _Aquí hay un simulador interactivo (`sensor-cola`): ábrelo en la web._

**Preguntas**

**7.** Si hubiera un sexto evento en t=50, ¿cuánto esperaría con la cola? (en ms, solo el número)

**8.** Con un evento cada 25 ms y un handler de 25 ms, ¿qué pasa con la cola?

   a) Crece sin parar, igual que antes
   b) Ningún evento espera: cada uno se atiende al llegar
   c) Se pierde uno de cada dos eventos
   d) La espera crece 25 ms por evento

### La cola absorbe ráfagas, no sobrecargas · Teoría

Una cola que se vacía a tiempo es una cola sana; una cola que solo crece es un sistema mal dimensionado.

| Situación | Qué hace la cola | Qué se nota |
| --- | --- | --- |
| **Ráfaga corta**: llegan 5 eventos juntos y luego silencio | Los guarda y el handler los va sacando | Una espera pasajera; la cola vuelve a quedar vacía |
| **Sobrecarga sostenida**: cada 10 ms llega uno y el handler tarda 25 ms | Crece sin parar | La espera aumenta evento a evento: 0, 15, 30, 45, 60 ms… |
| **Cola con límite**: la sobrecarga continúa y se llena | Ya no cabe nada más | Hay que descartar, y decidir qué |

<div class="cards">
  <div class="card"><h4>Cuándo la cola ayuda</h4><p>Cuando el trabajo medio que llega por segundo es menor que lo que el handler puede atender, aunque llegue a golpes: cargas de pantalla, muchos clics seguidos, mensajes de red agrupados.</p></div>
  <div class="card"><h4>Cuándo no arregla nada</h4><p>Cuando el productor es más rápido que el handler de forma permanente. La cola solo aplaza el problema y gasta memoria. Hace falta otro remedio: un handler más rápido, trabajo en paralelo o frenar al productor.</p></div>
</div>

> [!TRUCO] Dos números que vigilar
> - La **longitud de la cola**.
> - La **latencia** de cada evento: cuánto esperó antes de ejecutarse.
>
> Si ambos crecen sin parar, no es una ráfaga: es una sobrecarga.

> [!ANALOGIA]
> El pasador de la cocina aguanta bien el golpe de las 14:00, cuando entran diez mesas a la vez, porque a las 15:30 ya está vacío. Si entrasen diez mesas cada cuarto de hora toda la noche, ningún pasador, por grande que fuera, salvaría al cocinero.

**Preguntas**

**9.** ¿En qué situaciones **sí** ayuda una cola?

   a) Muchos clics seguidos y luego el usuario se queda leyendo
   b) Un sensor que envía cada 10 ms a un handler que tarda 25 ms, día y noche
   c) Mensajes de red que llegan agrupados de vez en cuando
   d) La carga de una pantalla que genera varios eventos de golpe

**10.** ¿Qué indica que un sistema no está sufriendo una ráfaga sino una sobrecarga?

   a) Que la cola tiene más de 5 eventos en algún momento
   b) Que la longitud de la cola y la latencia crecen sin parar
   c) Que algún evento espera más de 0 ms
   d) Que el handler tarda más de 10 ms

### El bucle completo con tabla de handlers (predice) · Ejercicio

Productores que encolan, un bucle que saca por orden y un **dispatcher** que elige el handler por tipo. Todo cabe en unas líneas de C++:

```cpp
#include <cstdio>
#include <queue>

enum Tipo { PULSADOR, TICK, SENSOR, N_TIPOS };
struct Evento { Tipo tipo; int dato; };
using Handler = void (*)(const Evento&);
std::queue<Evento> cola;

void alPulsar(const Evento& e) { std::printf("pulsador %d\n", e.dato); }
void alMedir(const Evento& e)  { std::printf("sensor %d C\n", e.dato); }
void alTick(const Evento& e) {
    std::printf("tick %d\n", e.dato);
    if (e.dato < 2) cola.push({TICK, e.dato + 1});
}

Handler tabla[N_TIPOS] = { alPulsar, alTick, alMedir };

int main() {
    cola.push({TICK, 1}); cola.push({SENSOR, 24}); cola.push({PULSADOR, 4});
    while (!cola.empty()) { Evento e = cola.front(); cola.pop(); tabla[e.tipo](e); }
}
```

Cómo leerlo:

- **El evento es tipo + dato.** `Tipo` es un `enum`, así que `PULSADOR` vale 0, `TICK` 1 y `SENSOR` 2. `N_TIPOS` (3) sirve para dimensionar la tabla.
- **La tabla de handlers** guarda un puntero a función por tipo, en el mismo orden que el `enum`.
- **El bucle** saca el primero (`front` + `pop`) y lo despacha.
- **`alTick` encola** un nuevo tick mientras el dato sea menor que 2.

Antes de mirar la salida en el paso siguiente, haz la traza de la cola en papel.

**Preguntas**

**11.** ¿Qué handler ejecuta `tabla[SENSOR](e)`?

   a) alPulsar
   b) alTick
   c) alMedir
   d) Ninguno: SENSOR no está en la tabla

**12.** Ordena las líneas que imprime el programa.

   Ordena: pulsador 4 · sensor 24 C · tick 1 · tick 2

### Un event loop completo en una línea · Teoría

La salida es:

```text
tick 1
sensor 24 C
pulsador 4
tick 2
```

Tres cosas que observar:

1. **Un handler puede encolar eventos nuevos**, que se colocan al final de la cola. El `tick 2` creado por el primer handler sale **el último**: espera a los que ya estaban.
2. **El dispatcher es una línea**: `tabla[e.tipo](e)`. El bucle no conoce la lógica de ningún evento; solo indexa la tabla.
3. **El evento es tipo + dato**, y el tipo indexa la tabla.

> [!IMPORTANTE]
> Esto **es** un event loop completo. Todo lo demás de la unidad (prioridad, descarte, handlers largos) son **decisiones sobre esta estructura**: qué tipo de cola usar, qué hacer cuando se llena, cuánto puede tardar cada handler.

Compáralo con la barra del bar de la Hora 1: `while (comandas.isNotEmpty())` + `removeFirst` + `preparar` es exactamente este `while (!cola.empty())` + `front/pop` + `tabla[...]`. La diferencia es que aquí, en lugar de un `when` dentro del handler, hay una **tabla de despacho** que elige el handler por tipo.

> [!TRUCO]
> Añadir un tipo de evento nuevo es añadir un valor al `enum`, una función y una entrada en la tabla. El bucle no se toca.

**Preguntas**

**13.** Escribe la expresión del programa que hace de **dispatcher**.

**14.** ¿Por qué `tick 2` sale el último aunque lo creó el primer handler?

   a) Porque los ticks tienen menos prioridad
   b) Porque se encola al final, detrás de los eventos que ya esperaban
   c) Porque `alTick` lo encola con retraso
   d) Porque el bucle atiende los eventos de dos en dos

### Prioridades: cuando lo urgente espera · Teoría

El orden de llegada es justo, pero **no distingue una alarma de un registro**. Supón cuatro eventos de log de 20 ms cada uno en la cola y que llega una alarma:

| Cola | Orden | La alarma se atiende a los |
| --- | --- | --- |
| Solo FIFO | log 1, log 2, log 3, log 4, ALARMA | **80 ms** |
| Con prioridad | ALARMA (prio 9), log 1, log 2, log 3, log 4 (prio 1) | **0 ms** |

![Tres filas. Solo FIFO: cuatro logs de 20 ms dibujados a escala delante de la ALARMA, que empieza a los 80 ms. Con prioridad: la ALARMA (prio 9) se coloca delante de los cuatro logs (prio 1) y se atiende a los 0 ms. Con prioridad y un log ya en marcha: la alarma llega mientras corre el log 1 y se coloca justo detrás de él, delante de los logs 2 a 4; adelanta, pero no interrumpe](/img/pde/tema-3/prioridad-sin-interrupcion.svg "La prioridad adelanta en la cola, pero no corta al handler que ya se ejecuta")

<div class="cards">
  <div class="card"><h4>Cómo funciona</h4><p>Cada evento lleva un nivel. Se inserta delante del primer evento menos urgente; dentro del mismo nivel, FIFO.</p></div>
  <div class="card"><h4>Lo que no hace</h4><p>No interrumpe el handler que ya corre: adelanta en la cola, no cancela lo que se ejecuta (run-to-completion).</p></div>
  <div class="card"><h4>El riesgo</h4><p>Si lo urgente llega sin parar, lo demás nunca se atiende (<b>inanición</b>). Se evita con límites o con <b>envejecimiento</b>.</p></div>
</div>

El **envejecimiento** (*aging*) consiste en ir subiendo la prioridad de un evento cuanto más tiempo lleva esperando, de modo que tarde o temprano adelante a los urgentes. Es lo que le faltaba a la `L21` del panel de la caldera.

> [!NOTA]
> Con 500 logs delante, la alarma de una cola FIFO tardaría **10 s**. En un microcontrolador, las interrupciones tienen **niveles** de prioridad exactamente por esta razón.

> [!EXAMEN]
> Prioridad ≠ interrupción. Una cola con prioridad **adelanta**; no corta al handler en curso. Y prioridad estricta sin remedio ⇒ riesgo de inanición.

**Preguntas**

**15.** Con 500 logs de 20 ms delante en una cola FIFO, ¿cuántos segundos espera la alarma? (solo el número)

**16.** ¿Qué medidas evitan la inanición en una cola con prioridad?

   a) Envejecimiento: subir la prioridad de lo que lleva mucho esperando
   b) Poner límites a lo urgente
   c) Dejar que la alarma interrumpa al handler en curso
   d) Usar una pila en lugar de una cola

### Fusión de eventos: coalescencia · Teoría

Si llegan muchos eventos **de estado** seguidos, basta con el último. Ejemplo con un mapa que se repinta:

| | Cola |
| --- | --- |
| **Sin fusionar** (5 repintados) | mover x=1, mover x=2, tocar marcador, mover x=3, mover x=4 |
| **Fusionando** (3 eventos, mismo resultado final) | mover x=2, tocar marcador, mover x=4 |

![Arriba, la cola sin fusionar con cinco eventos: mover x=1, mover x=2, tocar marcador, mover x=3 y mover x=4. Mover x=1 y mover x=2 se funden en mover x=2; mover x=3 y mover x=4 se funden en mover x=4; tocar marcador queda en medio y corta la fusión. Abajo, la cola fusionada con tres eventos](/img/pde/tema-3/coalescencia.svg "Cada mover se funde con el anterior solo si es el último de la cola: el «tocar» corta la fusión")

<div class="cards">
  <div class="card"><h4>Se funden: eventos de estado</h4><p>«Dónde está el dedo», «cuánto lleva descargado», «cuánto scroll»: el valor nuevo sustituye al anterior, así que el intermedio no aporta nada.</p></div>
  <div class="card"><h4>No se funden: eventos de acción</h4><p>«Pulsar Comprar», «enviar mensaje»: cada uno cuenta. Fundir dos clics sería perder un pedido.</p></div>
</div>

> [!IMPORTANTE] Regla de la fusión
> Un evento nuevo **solo se funde con el último evento de la cola** y **solo si es del mismo tipo**. Si hay otro por medio, el orden se conserva: por eso el «tocar marcador» corta la fusión y `mover x=2` sobrevive.

¿Por qué no fundir `mover x=2` con `mover x=4` saltándose el «tocar»? Porque cambiaría el orden de los hechos: el marcador se tocó cuando el mapa estaba en `x=2`, no en `x=4`.

> [!NOTA]
> En Android esta fusión se hace a mano (`removeMessages()`), como veremos al final.

**Preguntas**

**17.** Con fusión, el bucle aún no ha sacado nada y llegan: scroll 10, scroll 20, clic Comprar, scroll 30, scroll 40, scroll 50. ¿Cuántos eventos quedan en la cola?

**18.** ¿Qué eventos se pueden fusionar?

   a) Progreso de una descarga (porcentaje)
   b) Pulsar «Comprar»
   c) Posición del dedo al arrastrar
   d) Enviar un mensaje de chat

### Cola llena: elegir qué se pierde y contarlo · Teoría

Con memoria limitada, el descarte es inevitable. La decisión es **qué se tira** y que **no pase desapercibido**.

Ejemplo: una cola de **3 posiciones** recibe 5 lecturas seguidas sin que el bucle saque ninguna. Con la política «descartar el más antiguo», las lecturas 1 y 2 son pisadas; en la cola quedan 3, 4 y 5, y el **contador de perdidas vale 2**.

| Política | Qué descarta | Úsala para | Ejemplo |
| --- | --- | --- | --- |
| **Descartar el más antiguo** | El que lleva más tiempo esperando | Estados: solo importa el último valor | Temperatura, GPS |
| **Rechazar el nuevo** | El que intenta entrar | Órdenes que no se pueden saltar ni reordenar | Comandos de un protocolo |
| **Frenar al productor** | Nada: se le hace esperar | No se puede perder nada | Fichero a red (U5) |

Por qué cada una encaja con su ejemplo:

- En una **temperatura** o un **GPS**, la lectura vieja ya no vale: mejor quedarse con la nueva.
- En un **protocolo de comandos**, saltarse una orden intermedia podría dejar al otro extremo en un estado inconsistente: mejor rechazar lo nuevo (y que el emisor lo sepa).
- Al enviar un **fichero por la red** no se puede perder ni un byte: se frena a quien lee el fichero hasta que haya sitio.

> [!CUIDADO]
> Descartar es una **decisión de diseño, no un accidente**. Elige la política y **registra siempre cuánto pierdes**: un descarte silencioso es un fallo escondido (como el sensor sin cola, que perdía 3 de 5 sin avisar).

**Preguntas**

**19.** Una cola de posiciones GPS se llena. ¿Qué política usarías?

   a) Descartar el más antiguo
   b) Rechazar el nuevo
   c) Frenar al productor
   d) Vaciar la cola entera

**20.** Una cola de comandos de un protocolo (que no se pueden saltar ni reordenar) se llena. ¿Qué política usarías?

   a) Descartar el más antiguo
   b) Fusionar los comandos iguales
   c) Rechazar el nuevo
   d) Descartar uno al azar

**21.** Cola de 3 posiciones, política «descartar el más antiguo». Llegan 7 lecturas y el bucle no saca ninguna. ¿Cuánto vale el contador de perdidas?

### Run-to-completion · Teoría

Un handler se ejecuta **entero** antes de que empiece el siguiente: nada lo interrumpe, **ni siquiera un evento urgente**.

![Línea temporal a escala de 0 a 30 ms con tres eventos. A llega en 0 ms y su handler ocupa de 0 a 30 ms. B llega en 10 ms y espera hasta que A termina en 30 ms; entonces se ejecuta. C llega poco después de B y espera detrás de B; se ejecuta cuando B termina. Los handlers nunca se solapan](/img/pde/tema-3/run-to-completion.svg "A corre entero; B y C esperan su turno en la cola")

<div class="cards">
  <div class="card"><h4>Lo que ganamos</h4><p>Los handlers no se pisan entre sí: no necesitan cerrojos para el estado que comparten. El orden de ejecución es predecible y fácil de probar.</p></div>
  <div class="card"><h4>Lo que pagamos</h4><p>Cada evento espera a todo lo que tenga por delante. Un solo handler lento retrasa a todos los demás.</p></div>
</div>

Ya lo has visto dos veces:

- En la barra del bar, el café del desayuno esperó al zumo.
- En el C++, el `tick 2` lo creó el primer handler, pero no empezó hasta que acabaron los que ya estaban en la cola.

> [!ANALOGIA]
> El cocinero no deja una tortilla a medio cuajar porque entre otra comanda. Termina la tortilla. Eso hace que nunca se mezclen dos platos en la misma sartén (no hay cerrojos), pero si alguien pide un cocido de tres horas, todos los demás esperan.

> [!EXAMEN]
> «¿Qué ganamos y qué pagamos con run-to-completion?» Ganamos: sin cerrojos, orden predecible. Pagamos: un handler lento retrasa a todos.

**Preguntas**

**22.** ¿Qué **ganamos** con run-to-completion?

   a) Los handlers no necesitan cerrojos para el estado compartido
   b) Un evento urgente se atiende al instante aunque haya otro handler en marcha
   c) El orden de ejecución es predecible y fácil de probar
   d) Un handler lento no afecta a los demás

**23.** A llega en 0 ms y su handler dura 30 ms; B llega en 10 ms y su handler dura 5 ms. ¿Cuándo termina el handler de B?

   a) 15 ms
   b) 30 ms
   c) 35 ms
   d) 40 ms

### La pantalla que tarda en responder · Ejercicio

Una tienda web: una carga de **100 ms** en el bucle y, mientras, los clics del usuario van llegando. Cada evento tiene su instante de llegada `t` y lo que cuesta su handler.

```js
const eventos = [
  { t: 0,  nombre: "cargarCatalogo", coste: 100 },
  { t: 16, nombre: "clic Comprar",   coste: 2 },
  { t: 32, nombre: "scroll",         coste: 2 },
  { t: 48, nombre: "clic Carrito",   coste: 2 },
];
let reloj = 0;

for (const e of eventos) {
  reloj = Math.max(reloj, e.t);
  console.log(`${e.nombre.padEnd(15)} espera ${reloj - e.t} ms`);
  reloj += e.coste;
}
```

El razonamiento es el mismo que con el sensor: `reloj` es cuándo queda libre el bucle; cada evento empieza en `max(reloj, t)` y espera la diferencia con su llegada.

Haz la traza:

| Evento | Llega (t) | Empieza | Espera |
| --- | --- | --- | --- |
| cargarCatalogo | 0 | 0 | 0 |
| clic Comprar | 16 | ? | ? |
| scroll | 32 | ? | ? |
| clic Carrito | 48 | ? | ? |

> [!NOTA]
> El usuario percibe más de **100 ms** como «va lento», y a los **5 s** Android muestra «la aplicación no responde». ¿Cómo evitarías que la carga bloquee los clics **sin cambiar el bucle**?

**Preguntas**

**24.** ¿Cuántos ms espera «clic Comprar»?

**25.** ¿Cuántos ms espera «clic Carrito»?

### Trocear el trabajo · Ejercicio

La salida del ejemplo anterior fue:

```text
cargarCatalogo  espera 0 ms
clic Comprar    espera 84 ms
scroll          espera 70 ms
clic Carrito    espera 56 ms
```

Los clics cuestan 2 ms, pero esperan 84, 70 y 56 ms porque un handler de 100 ms bloquea el bucle. La solución sin tocar el bucle: **trocear la carga** en 10 trozos de 10 ms. Cada trozo, al terminar, **se vuelve a encolar** y los clics se cuelan entre ellos.

```js
const cola = [
  { t: 0,  nombre: "cargar",       coste: 10, trozos: 10 },
  { t: 16, nombre: "clic Comprar", coste: 2 },
  { t: 32, nombre: "scroll",       coste: 2 },
  { t: 48, nombre: "clic Carrito", coste: 2 },
];
let reloj = 0;

while (cola.length) {
  const e = cola.shift(); reloj = Math.max(reloj, e.t);
  if (!e.trozos) console.log(`${e.nombre.padEnd(13)} espera ${reloj - e.t} ms`);
  reloj += e.coste;
  if (e.trozos > 1) {
    const sig = { ...e, t: reloj, trozos: e.trozos - 1 };
    const i = cola.findIndex((x) => x.t > sig.t);
    cola.splice(i < 0 ? cola.length : i, 0, sig);
  }
}
console.log(`carga terminada en ${reloj} ms`);
```

1. **Cada trozo termina y devuelve el control**: entre dos trozos el bucle atiende otros eventos.
2. **El siguiente trozo se encola** detrás de lo que ya esperaba (se inserta por instante `t`).
3. **El coste es mínimo**.

```text
clic Comprar  espera 4 ms
scroll        espera 0 ms
clic Carrito  espera 6 ms
carga terminada en 106 ms
```

> _Aquí hay un simulador interactivo (`trocear`): ábrelo en la web._
> [!IMPORTANTE]
> Un handler **decide, delega o hace un trozo**. Los clics esperan 0–6 ms (frente a 56–84) y la carga termina casi a la vez.

**Preguntas**

**26.** ¿Por qué la carga troceada termina en 106 ms y no en 100?

   a) Por el coste de reencolar cada trozo
   b) Por los 6 ms de los tres clics atendidos entre trozos
   c) Porque cada trozo espera 0,6 ms al bucle
   d) Porque el último trozo dura 16 ms

**27.** ¿Por qué «clic Comprar» espera 4 ms y no 0?

   a) Porque llega en 16, a mitad del segundo trozo (10–20), y tiene que esperar a que termine
   b) Porque los clics tienen menos prioridad que la carga
   c) Porque el bucle tarda 4 ms en sacar un evento
   d) Porque antes se atiende el scroll

### Cómo se nota un handler largo · Teoría

La misma causa produce **síntomas distintos** según el sistema:

| Síntoma | Qué pasa por dentro | Dónde se nota |
| --- | --- | --- |
| **Latencia** | Los eventos esperan en la cola mientras el handler corre | Un toque tarda en reaccionar |
| **Pérdida** | La cola se llena y se descarta | Lecturas de sensor que faltan |
| **Congelación** | El bucle no vuelve a mirar la cola | Ventana «no responde», ANR en Android |
| **Reinicio** | El watchdog no recibe su señal a tiempo | Un firmware que se resetea solo |

Y tiene **tres salidas**:

<div class="cards">
  <div class="card"><h4>Trocear</h4><p>Hacer un trozo, reencolar el resto y volver (lo que acabas de ver).</p></div>
  <div class="card"><h4>Delegar</h4><p>Pasar el trabajo a otro hilo o tarea y volver con el resultado (U5 y U6).</p></div>
  <div class="card"><h4>Acotar y medir</h4><p>Presupuesto de tiempo por handler y aviso si se supera.</p></div>
</div>

> [!IMPORTANTE]
> La capacidad de respuesta es una propiedad del **peor** handler, no del handler medio. Basta un handler de 3 s entre mil de 1 ms para que la app se congele 3 s.

El handler **no es el sitio del trabajo lento**. Lo que debe hacer es: leer la entrada, actualizar el estado y encolar lo siguiente.

> [!NOTA] El watchdog
> En un microcontrolador, el *watchdog* es un temporizador que reinicia el sistema si el programa no le «da una señal» periódicamente. Si un handler bloquea el bucle demasiado tiempo, nadie da esa señal y el firmware se reinicia.

**Preguntas**

**28.** Un firmware se resetea solo cada vez que procesa un fichero grande dentro de un handler. ¿Qué síntoma es?

   a) Latencia
   b) Pérdida
   c) Congelación
   d) Reinicio por watchdog

**29.** La capacidad de respuesta de un sistema por eventos depende de...

   a) La duración media de los handlers
   b) La duración del peor handler
   c) El número de productores
   d) El tamaño máximo de la cola

### Android: Looper, Handler y MessageQueue · Teoría

El **hilo principal** (UI thread) de una app Android tiene un **Looper** que saca mensajes de su **MessageQueue** y se los da a un **Handler**.

<div class="flow">
  <div><b>Sistema / otros hilos</b><small>toques, vsync · handler.post(…)</small></div>
  <div><b>MessageQueue</b><small>ordenada por hora</small></div>
  <div><b>Looper.loop()</b><small>bucle del hilo</small></div>
  <div><b>Handler</b><small>handleMessage / Runnable</small></div>
</div>

| Pieza genérica | En Android | Detalle |
| --- | --- | --- |
| Evento / mensaje | `Message` (`what`, `arg1`, `obj`) o `Runnable` | Un `Runnable` es un callback listo para ejecutar |
| Cola | `MessageQueue` | Ordenada por **hora de entrega** (`when`), **no por prioridad** |
| Bucle | `Looper.loop()` | Un Looper por hilo; la app arranca con el del hilo principal |
| Dispatcher + handler | `Handler.dispatchMessage()` | Ligado al Looper de un hilo |
| Fusión / descarte | `removeMessages()`, `postAtFrontOfQueue()` | Se hace a mano, como en la coalescencia |

> [!IMPORTANTE]
> Cada `onClick`, cada `onCreate` y cada dibujo se ejecuta **dentro de un handler de este bucle**. Por eso un `onClick` lento congela toda la app.

> [!CUIDADO] postDelayed no espera
> `postDelayed(r, 500)` **no** espera 500 ms: inserta el mensaje en la cola con `when = ahora + 500` y vuelve en el acto. El Looper lo sacará cuando llegue esa hora... **y el hilo esté libre**. Si en ese momento hay un handler largo en marcha, se ejecutará más tarde.

> [!EXAMEN]
> «¿Por qué criterio ordena Android su MessageQueue?» Por `when` (hora de entrega), no por prioridad.

**Preguntas**

**30.** ¿Cómo está ordenada la `MessageQueue` de Android?

   a) Por prioridad del mensaje
   b) Por hora de entrega (`when`)
   c) Por el hilo que publicó el mensaje
   d) Es una pila: sale el último publicado

**31.** ¿Qué clase de Android hace de **bucle de eventos** de un hilo?

**32.** Llamas a `postDelayed(r, 500)` y justo después el hilo principal ejecuta un handler de 2 s. ¿Cuándo se ejecuta `r`?

   a) A los 500 ms exactos, interrumpiendo el handler
   b) Cuando termine el handler de 2 s, porque `r` solo puede salir con el hilo libre
   c) Nunca: el mensaje caduca
   d) Inmediatamente, porque postDelayed bloquea 500 ms y luego ejecuta

### El presupuesto por fotograma · Teoría

A **60 Hz** hay **16,7 ms** entre dos señales *vsync* para atender los eventos y dibujar. Si un handler tarda más, se pierden fotogramas.

| Pantalla | Presupuesto por fotograma | Qué entra en él |
| --- | --- | --- |
| 60 Hz | 16,7 ms | Eventos de entrada + layout + dibujo |
| 90 Hz | 11,1 ms | Lo mismo, con menos margen |
| 120 Hz | 8,3 ms | Cada handler debe durar pocos ms |

El presupuesto es simplemente 1000 ms / frecuencia: 1000 / 60 ≈ 16,7.

### Un onClick de 42 ms

Con vsync cada ~16,7 ms (0, 16, 33, 50, 66 ms…), un `onClick` lento de **42 ms** ocupa el hilo principal durante más de dos fotogramas enteros. Mientras corre, el hilo no puede dibujar, así que se pierden **2 fotogramas** y la animación da un tirón.

![Línea temporal a escala con señales vsync en 0; 16,7; 33,3; 50 y 66,7 ms. En el hilo principal, un onClick ocupa de 0 a 42 ms. Los fotogramas de 16,7 y 33,3 ms caen mientras corre el onClick y se pierden; el de 50 ms, con el hilo ya libre, se dibuja. Una llave marca el presupuesto de 16,7 ms entre dos vsync](/img/pde/tema-3/fotograma-onclick-42ms.svg "Un onClick de 42 ms tapa dos vsync: dos fotogramas perdidos")

> [!IMPORTANTE] Dos umbrales
> - Por encima de **16 ms** (a 60 Hz) el usuario nota el tirón: *jank*.
> - A partir de **~5 s** sin responder, el sistema muestra **ANR** («la aplicación no responde»).

> [!TRUCO]
> El presupuesto no es solo para tu handler: en esos 16,7 ms también tienen que caber el layout y el dibujo. Un handler «de 15 ms» ya es demasiado.

**Preguntas**

**33.** ¿Cuál es el presupuesto por fotograma a 120 Hz? (en ms, con un decimal)

**34.** Un handler del hilo principal tarda 300 ms. ¿Qué nota el usuario en una pantalla de 60 Hz?

   a) Nada, porque es menos de 5 s
   b) Jank: se pierden fotogramas, aunque no llega a ANR
   c) Un ANR inmediato
   d) Un reinicio de la app por watchdog

### El botón que descarga en el hilo principal · Ejercicio

Una app de reparto: al tocar «Actualizar catálogo», la app se queda congelada durante la descarga.

```kotlin
boton.setOnClickListener {
    val datos = descargarCatalogo()   // 3 s de red
    lista.submit(datos)
}
```

1. **El `onClick` corre en el hilo principal.** Durante 3 s no se atiende ningún otro evento.
2. **La lista no se actualiza hasta que acaba.** Ni scroll, ni animaciones, ni más toques.

Aplica lo que llevas visto:

- El `onClick` es un handler del `Looper` principal. Mientras está dentro de `descargarCatalogo()`, el Looper no puede sacar ningún mensaje más de la `MessageQueue`.
- Los toques, el scroll y los vsync siguen llegando… y se acumulan en la cola.
- Con 60 Hz, en 3 s caben unos 180 fotogramas: ninguno se dibuja.

Piensa antes de contestar: **¿qué ve el usuario en esos 3 s? ¿Y si la red tarda 8 s? ¿Dónde debería ejecutarse la descarga y cómo vuelve el resultado a la lista?**

**Preguntas**

**35.** ¿Qué ve el usuario durante los 3 s de descarga?

   a) La app funciona normal y la lista aparece a los 3 s
   b) Una app congelada: no hace scroll, no anima y no responde a los toques
   c) Un mensaje de error de red
   d) La lista vacía, pero puede seguir usando la app

**36.** ¿Y si la red tarda 8 s?

   a) Lo mismo, solo que más rato; el sistema no hace nada
   b) Android cancela la descarga a los 5 s
   c) Se supera el umbral de ~5 s y el sistema puede mostrar un ANR («la aplicación no responde»)
   d) La app se reinicia por watchdog

### Trabajo fuera, resultado dentro · Teoría

La descarga va a **otro hilo**, y el resultado **vuelve al hilo principal publicando en su Handler**.

```kotlin
val principal = Handler(Looper.getMainLooper())

boton.setOnClickListener {
    thread {
        val datos = descargarCatalogo()          // 3 s, sin molestar
        principal.post { lista.submit(datos) }
    }
}
```

1. **Handler ligado al Looper principal.** `Handler(Looper.getMainLooper())` es la «puerta de entrada» a la cola del hilo de la UI.
2. **El trabajo lento sale del bucle.** El `onClick` solo arranca el hilo: termina en microsegundos.
3. **El resultado se encola.** `principal.post { … }` mete un `Runnable` en la `MessageQueue` del hilo principal; cuando le toque, el Looper lo ejecuta y la lista se actualiza **desde el hilo principal, el único que puede tocar la UI**.

![Dos carriles en el tiempo. En el hilo principal, onClick lanza thread { } y termina enseguida; después el hilo sigue atendiendo toques y scroll. En otro hilo, descargarCatalogo() tarda 3 s. Al acabar, principal.post { … } encola el resultado en el hilo principal, donde se ejecuta lista.submit](/img/pde/tema-3/trabajo-fuera-resultado-dentro.svg "El trabajo lento va a otro hilo; el resultado vuelve como un evento más con principal.post")

> [!IMPORTANTE] Dos reglas
> - **Nunca bloquear el hilo principal.**
> - **Nunca tocar la UI desde otro hilo.**

Es la salida «delegar» de la tabla de handlers largos. Y fíjate en que el resultado vuelve **como un evento más**: el patrón productor → cola → bucle → handler aparece de nuevo, con el hilo de descarga como productor.

> [!NOTA]
> Las corrutinas y `async`/`await` (Unidad 6) hacen lo mismo con menos código.

**Preguntas**

**37.** ¿Por qué no se llama a `lista.submit(datos)` directamente dentro de `thread { }`?

   a) Porque sería más lento
   b) Porque solo el hilo principal puede tocar la UI; hay que encolar el cambio en su Looper
   c) Porque `thread` no puede acceder a variables de fuera
   d) Porque `submit` solo acepta Runnables

**38.** Marca lo que es cierto de la solución.

   a) El `onClick` termina casi al instante
   b) La descarga se ejecuta en el hilo principal pero troceada
   c) La actualización de la lista se ejecuta en el hilo principal
   d) Durante la descarga, la app sigue haciendo scroll y atendiendo toques

### Lista de comprobación y puente a la U4 · Teoría

Seis preguntas para **diseñar o depurar** un sistema por eventos:

| # | Pregunta | Respuesta sana |
| --- | --- | --- |
| 1 | ¿Quién produce y qué hace? | Los productores solo encolan: rápido y sin lógica. |
| 2 | ¿Qué orden y qué prioridad? | FIFO, salvo urgencias; sin inanición. |
| 3 | ¿Qué se fusiona o se descarta? | Solo eventos de estado; y se mide lo perdido. |
| 4 | ¿Cuánto dura el peor handler? | Esa es tu latencia de respuesta. |
| 5 | ¿Dónde va el trabajo largo? | Trocear o delegar; nunca en el bucle. |
| 6 | ¿Cuál es el presupuesto? | 16,7 ms por fotograma, o el plazo del sistema. |

> [!TRUCO]
> Úsala como guion cuando te den un enunciado de diseño en el examen: responder a las seis preguntas suele cubrir todo lo que se pide.

### Puente a la Unidad 4

Ya sabemos que los eventos llegan **de uno en uno y en orden**. Pero un mismo evento («pulsar») debe hacer **cosas distintas según el momento**: el mismo botón enciende si está apagado y apaga si está encendido; el mismo «SUBIR» del panel puede subir la temperatura o cambiar de menú.

Eso no se resuelve con más colas ni más prioridades: se modela con **máquinas de estado**, el tema de la U4.

**Preguntas**

**39.** Ordena las seis preguntas de la lista de comprobación.

   Ordena: ¿Cuál es el presupuesto? · ¿Cuánto dura el peor handler? · ¿Dónde va el trabajo largo? · ¿Quién produce y qué hace? · ¿Qué orden y qué prioridad? · ¿Qué se fusiona o se descarta?

**40.** ¿Qué problema deja abierto la U3 para la U4?

   a) Cómo ejecutar varios handlers a la vez
   b) Que un mismo evento debe hacer cosas distintas según el momento
   c) Cómo evitar la inanición
   d) Cómo descargar datos sin bloquear

### Repaso: bucle de eventos, colas y dispatching · Repaso

> [!RESUMEN] Chuleta
> - **Idea**: el handler se ejecuta cuando le toca; lo decide la cola.
> - **Piezas**: evento (tipo + datos, inmutable), productor (rápido, solo encola), cola (orden, prioridad, descarte), bucle + dispatcher (un hilo; `tabla[e.tipo](e)`).
> - **Sin cola** se pierde; **con cola** se espera. La cola absorbe ráfagas, **no sobrecargas**: vigila longitud y latencia.
> - **Prioridad**: se inserta delante del primero menos urgente; adelanta, no interrumpe. Riesgo: inanición → límites o envejecimiento.
> - **Coalescencia**: solo eventos de estado, solo con el último de la cola y del mismo tipo.
> - **Cola llena**: descartar el más antiguo (estados), rechazar el nuevo (órdenes), frenar al productor (no perder nada). Contar siempre lo perdido.
> - **Run-to-completion**: sin cerrojos y predecible; un handler lento retrasa a todos.
> - **Handler largo**: latencia, pérdida, congelación/ANR, reinicio. Remedios: trocear, delegar, acotar y medir.
> - **Android**: `Looper` + `MessageQueue` (por `when`) + `Handler`. 16,7 ms por fotograma a 60 Hz; ANR a ~5 s. Trabajo fuera, resultado dentro con `post`.

| Concepto | En una frase |
| --- | --- |
| Ráfaga | Llega mucho de golpe, pero de media da tiempo |
| Sobrecarga | Llega más de lo que se atiende, siempre |
| Inanición | Lo poco urgente no se atiende nunca |
| Jank | Fotogramas perdidos por un handler > 16 ms |
| ANR | Hilo principal bloqueado ~5 s |

**Preguntas**

**41.** Un sensor produce cada 5 ms y su handler tarda 8 ms de forma permanente. ¿Qué afirmación es correcta?

   a) Una cola suficientemente grande lo resuelve
   b) La cola solo aplaza el problema: hace falta un handler más rápido, paralelismo o frenar al productor
   c) Basta con poner prioridad a los eventos del sensor
   d) Con run-to-completion no se acumulan eventos

**42.** Marca las afirmaciones correctas.

   a) Una cola con prioridad interrumpe al handler en curso si llega algo urgente
   b) Dos eventos «mover» seguidos al final de la cola se pueden fundir
   c) Dos clics en «Comprar» seguidos se pueden fundir
   d) Descartar sin contar lo perdido es un fallo escondido

**43.** ¿Cuál es el presupuesto por fotograma a 60 Hz? (en ms, con un decimal)

**44.** En Android, ¿cómo se actualiza la UI con el resultado de una descarga hecha en otro hilo?

   a) Llamando a la vista directamente desde el hilo de descarga
   b) Publicando un Runnable en un `Handler` ligado al Looper principal (`post`)
   c) Usando `postAtFrontOfQueue` en el hilo de descarga para que se ejecute ahí
   d) Bloqueando el hilo principal hasta que acabe la descarga

### Solucionario

1. b) Cuando le toca, según lo decida la cola  
   Entre que algo ocurre y que se atiende está la cola. El momento de ejecución lo decide ella (orden de llegada, prioridad...), no el instante del suceso.
2. d) El handler  
   La receta es el handler: la lógica concreta para cada tipo. El cocinero (bucle) solo coge comandas y aplica la receta que toque.
3. a) El productor debe ser rápido y no ejecutar la reacción; c) La cola define orden, prioridad y qué se descarta  
   El evento es un aviso pequeño e inmutable (no un trabajo) y el bucle lo recorre **un único hilo**. Productor rápido y cola que decide orden, prioridad y descarte son las reglas correctas.
4. b) Por su tipo  
   El dispatcher mira el **tipo** del evento y elige su handler. La prioridad la usa la cola para decidir el orden de salida, no el dispatcher.
5. b) t=10; c) t=20; e) t=40  
   t=0 se atiende (libre en 25). t=10 y t=20 llegan con el handler ocupado: perdidos. t=30 ≥ 25: atendido (libre en 55). t=40 < 55: perdido. **3 de 5** desaparecen y nadie se entera.
6. b) Una cola, sacando por orden de llegada (FIFO)  
   Una **cola FIFO** da a cada evento un sitio donde esperar su turno. Hacer el handler más rápido también ayudaría, pero sin cola cualquier ráfaga seguiría perdiendo eventos; y una variable con el último valor solo sirve para eventos de estado.
7. `75`  
   El reloj llega a 100 + 25 = 125, así que el evento de t=50 se atiende en 125: espera **75 ms**. La espera sigue creciendo 15 ms por evento.
8. b) Ningún evento espera: cada uno se atiende al llegar  
   Cada evento llega justo cuando el handler queda libre: espera 0 y la cola nunca acumula. La espera crece solo cuando el handler es más lento que el ritmo de llegada. Puedes comprobarlo en el simulador.
9. a) Muchos clics seguidos y luego el usuario se queda leyendo; c) Mensajes de red que llegan agrupados de vez en cuando; d) La carga de una pantalla que genera varios eventos de golpe  
   Las tres situaciones de ráfaga tienen un ritmo medio asumible. El sensor permanente es una **sobrecarga sostenida**: la cola solo aplaza el problema.
10. b) Que la longitud de la cola y la latencia crecen sin parar  
   En una ráfaga la cola sube y luego vuelve a vaciarse. Si longitud y latencia crecen **sin parar**, el handler no da abasto de forma permanente.
11. c) alMedir  
   `SENSOR` es el índice 2 y `tabla[2]` es `alMedir`. Por eso el orden de la tabla tiene que coincidir con el del `enum`.
12. 1. tick 1 → 2. sensor 24 C → 3. pulsador 4 → 4. tick 2  
   La cola empieza con TICK 1, SENSOR 24 y PULSADOR 4. Al atender TICK 1 se encola TICK 2 **al final**, detrás de los que ya estaban.
13. `tabla[e.tipo](e)`  
   `tabla[e.tipo](e)`: indexa la tabla con el tipo del evento y llama al puntero a función obtenido pasándole el evento.
14. b) Porque se encola al final, detrás de los eventos que ya esperaban  
   `std::queue` es FIFO: `push` coloca al final. SENSOR y PULSADOR ya estaban en la cola, así que salen antes que el tick nuevo.
15. `10`  
   500 × 20 ms = 10 000 ms = **10 s**. Con prioridad, 0 ms (si no hay ningún handler en curso).
16. a) Envejecimiento: subir la prioridad de lo que lleva mucho esperando; b) Poner límites a lo urgente  
   Límites y envejecimiento son los remedios citados. Interrumpir al handler rompería run-to-completion y no resuelve la inanición; una pila empeoraría la justicia del orden.
17. `3`  
   scroll 20 sustituye a scroll 10; el clic corta la fusión; scroll 30, 40 y 50 se funden en scroll 50. Quedan **3**: scroll 20, clic Comprar, scroll 50.
18. a) Progreso de una descarga (porcentaje); c) Posición del dedo al arrastrar  
   Progreso y posición son **estado**: solo importa el último valor. Comprar y enviar son **acciones**: fundirlas sería perder un pedido o un mensaje.
19. a) Descartar el más antiguo  
   La posición es un **estado**: solo importa el último valor. Se tira la más antigua.
20. c) Rechazar el nuevo  
   Rechazar el nuevo mantiene íntegra la secuencia ya aceptada; el emisor sabe que su comando no entró. Tirar uno antiguo rompería el orden.
21. `4`  
   Caben 3 (las lecturas 5, 6 y 7); las 4 primeras se pisan. Contador = **4**.
22. a) Los handlers no necesitan cerrojos para el estado compartido; c) El orden de ejecución es predecible y fácil de probar  
   Sin interrupciones no hay carreras entre handlers, y el orden es reproducible. Pero lo urgente **espera** y un handler lento **sí** retrasa a todos: eso es lo que pagamos.
23. c) 35 ms  
   B espera de 10 a 30 ms (20 ms de latencia), se ejecuta de 30 a 35 ms.
24. `84`  
   Llega en 16 y empieza en 100: espera **84 ms**, aunque su handler cuesta solo 2 ms.
25. `56`  
   Empieza en 104 (100 + 2 + 2) y llegó en 48: espera **56 ms**. El scroll espera 70 (empieza en 102, llegó en 32).
26. b) Por los 6 ms de los tres clics atendidos entre trozos  
   100 ms de carga + 3 eventos de 2 ms intercalados = 106 ms. El simulador no cobra nada por reencolar.
27. a) Porque llega en 16, a mitad del segundo trozo (10–20), y tiene que esperar a que termine  
   El segundo trozo corre de 10 a 20 ms y no se interrumpe: el clic llega en 16 y empieza en 20. Trocear acota la espera al tamaño de un trozo, no la elimina.
28. d) Reinicio por watchdog  
   El handler largo impide que el bucle dé la señal al watchdog a tiempo, y este reinicia el sistema.
29. b) La duración del peor handler  
   Cualquier evento puede caer justo detrás del handler más lento. Por eso la latencia de respuesta la marca el **peor** handler.
30. b) Por hora de entrega (`when`)  
   Por `when`. Un `post` normal usa `when = ahora`, y `postDelayed` lo coloca más adelante en el tiempo.
31. `Looper`  
   `Looper`: hay uno por hilo y `Looper.loop()` saca mensajes de la `MessageQueue` y los despacha.
32. b) Cuando termine el handler de 2 s, porque `r` solo puede salir con el hilo libre  
   `r` queda en la cola con `when = ahora + 500`, pero el Looper no puede sacarlo mientras el handler de 2 s se ejecuta. Se ejecuta al terminar este, con retraso.
33. `8,3`  
   1000 ms / 120 ≈ **8,3 ms** para eventos, layout y dibujo.
34. b) Jank: se pierden fotogramas, aunque no llega a ANR  
   300 ms supera con mucho los 16,7 ms: se pierden fotogramas y la app «va lenta». No llega a los ~5 s del ANR.
35. b) Una app congelada: no hace scroll, no anima y no responde a los toques  
   El hilo principal está bloqueado dentro del `onClick`: ni dibuja ni atiende eventos. Todo se queda quieto hasta que la descarga termina.
36. c) Se supera el umbral de ~5 s y el sistema puede mostrar un ANR («la aplicación no responde»)  
   Más de ~5 s sin que el hilo principal responda ⇒ ANR. El usuario puede elegir cerrar la app.
37. b) Porque solo el hilo principal puede tocar la UI; hay que encolar el cambio en su Looper  
   Tocar la UI desde otro hilo está prohibido. `principal.post { … }` convierte la actualización en un mensaje de la cola del hilo principal.
38. a) El `onClick` termina casi al instante; c) La actualización de la lista se ejecuta en el hilo principal; d) Durante la descarga, la app sigue haciendo scroll y atendiendo toques  
   La descarga no se trocea: se **delega** a otro hilo. El hilo principal queda libre durante esos 3 s y solo recibe, al final, el `Runnable` con el resultado.
39. 1. ¿Quién produce y qué hace? → 2. ¿Qué orden y qué prioridad? → 3. ¿Qué se fusiona o se descarta? → 4. ¿Cuánto dura el peor handler? → 5. ¿Dónde va el trabajo largo? → 6. ¿Cuál es el presupuesto?  
   Productor → cola (orden, fusión, descarte) → handler (peor caso, trabajo largo) → presupuesto. Siguen el recorrido de un evento.
40. b) Que un mismo evento debe hacer cosas distintas según el momento  
   La reacción a un evento depende del estado del sistema: eso se modela con máquinas de estado (U4).
41. b) La cola solo aplaza el problema: hace falta un handler más rápido, paralelismo o frenar al productor  
   Es una sobrecarga sostenida: la espera y la cola crecen sin límite. La cola absorbe ráfagas, no sobrecargas.
42. b) Dos eventos «mover» seguidos al final de la cola se pueden fundir; d) Descartar sin contar lo perdido es un fallo escondido  
   La prioridad adelanta pero no interrumpe; los eventos de acción nunca se funden. Fundir estados y contar los descartes son buenas prácticas.
43. `16,7`  
   1000 / 60 ≈ **16,7 ms** para eventos de entrada, layout y dibujo.
44. b) Publicando un Runnable en un `Handler` ligado al Looper principal (`post`)  
   `Handler(Looper.getMainLooper()).post { … }` encola el cambio en el hilo principal, el único que puede tocar la UI.

---

# Tema L1 · Laboratorio · ESP32 emulado y Bluetooth

Del ESP32 emulado a tu app Android: un firmware que corre en esp-emu manda un tick cada 5 s y responde con eco por Bluetooth clásico (SPP) gracias a Bumble, y una Activity Kotlin lo recibe como eventos que llegan desde otro hilo.

## Montar la cadena: firmware, esp-emu, Bumble y emulador

_Qué tienes que conseguir, las cuatro piezas del laboratorio, por qué hace falta Bumble, la cadena SPP completa con su orden de arranque, los siete pasos con hitos verificables y cómo se trabaja el firmware._

### A dónde tienes que llegar · Teoría

En este laboratorio tu app Android **conversa con un ESP32 que no existe físicamente**. El firmware que compilas con PlatformIO corre en un **emulador de ESP32-C3** dentro de tu ordenador y se comunica con la app por **Bluetooth clásico (SPP)**, exactamente igual que lo haría con un módulo **HC-05** real.

El comportamiento es sencillo a propósito:

- **Cada 5 segundos** el firmware manda un `tick` con los segundos que lleva en marcha.
- Cuando le escribes algo desde la app, **contesta con un eco** numerado.

Esto es lo que verás en la app (emulador Pixel 8, conectada a `ESP32-EMU-SPP`):

```text
<< tick 370 s
<< tick 375 s
>> hola desde la app
<< eco #4: hola desde la app
```

Las flechas `<<` marcan lo que **llega** del ESP32 y `>>` lo que **envías** tú.

> [!ANALOGIA]
> Es como ensayar una obra de teatro con un actor de cartón: el decorado, el guion y el público (tu app) son reales, pero el actor que responde lo pone el ordenador. Si la obra funciona con el actor de cartón, funcionará con el de carne y hueso.

### ¿Por qué es interesante para esta asignatura?

Hasta ahora casi todos tus eventos venían **del dedo**: `onClick`, `onCheckedChanged`... Aquí aparecen dos fuentes nuevas:

<div class="cards">
  <div class="card"><h4>Evento temporal</h4><p>El tick cada 5 s: nadie lo pide, llega solo.</p></div>
  <div class="card"><h4>Evento externo</h4><p>Bytes que llegan por un socket cuando quieren, y en otro hilo.</p></div>
  <div class="card"><h4>Evento de usuario</h4><p>Tu mensaje: lo escribes y lo envías, y provoca un eco.</p></div>
</div>

**Preguntas**

**1.** Sin tocar la app, ¿qué deberías ver aparecer en ella cuando todo funciona?

   a) Nada: el ESP32 solo responde cuando le escribes
   b) Un `eco #N` cada 5 segundos
   c) Un `tick N s` cada 5 segundos
   d) Un mensaje cada vez que pulsas el botón BOOT

**2.** ¿Por qué tipo de enlace hablan la app y el ESP32 emulado?

   a) Wi-Fi, con peticiones HTTP
   b) Bluetooth clásico con el perfil SPP, como un HC-05
   c) Bluetooth Low Energy con GATT
   d) Cable USB emulado

### Cuatro piezas, cuatro herramientas · Teoría

La práctica se monta con cuatro piezas. **Solo esp-emu y Bumble son nuevas**; el resto ya lo tienes del laboratorio anterior.

<div class="cards">
  <div class="card"><h4>1 · Firmware</h4><p><b>VS Code + PlatformIO.</b> Proyecto <code>demo-esp32</code>, placa ESP32-C3, framework Arduino. Compila y genera la imagen de flash que carga el emulador.</p></div>
  <div class="card"><h4>2 · Chip</h4><p><b>esp-emu (Espressif).</b> Emulador oficial de ESP32-C3/C6/H2/P4. Ejecuta tu firmware y expone la UART1 por TCP. Es lo nuevo de esta práctica.</p></div>
  <div class="card"><h4>3 · Radio</h4><p><b>Bumble (Google).</b> Pila Bluetooth en Python. Hace de HC-05 virtual: convierte la UART del firmware en un servicio SPP visible desde Android.</p></div>
  <div class="card"><h4>4 · App</h4><p><b>Android Studio.</b> La plantilla <code>pde-plantilla-sdk36</code> con una Activity nueva, <code>BluetoothActivity</code>. El emulador Pixel 8 (API 36) aporta la radio virtual (<i>netsim</i>).</p></div>
</div>

> [!IMPORTANTE] Regla de oro
> **Tú escribes los dos extremos**: el firmware (C++/Arduino) y la Activity (Kotlin). Lo de en medio (esp-emu, Bumble y el emulador Android) **solo se arranca**: viene configurado en los scripts.

Esta regla te ahorra mucho tiempo: si algo falla en medio, casi nunca es «tu código», sino el orden de arranque o una dirección. Lo veremos en el diagnóstico.

| Pieza | Lenguaje / herramienta | ¿La programas tú? |
| --- | --- | --- |
| Firmware | C++ / Arduino en PlatformIO | **Sí** |
| esp-emu | Binario de Espressif | No, solo se arranca |
| Bumble | Paquete Python | No, solo se arranca |
| Emulador Android + netsim | Android Studio | No, solo se arranca |
| BluetoothActivity | Kotlin | **Sí** |

**Preguntas**

**3.** Según la regla de oro, ¿qué piezas **escribes tú**?

   a) El firmware `src/spp/main.cpp`
   b) El emulador esp-emu
   c) El puente Bumble
   d) La `BluetoothActivity` en Kotlin
   e) La radio virtual netsim

**4.** ¿Qué pieza aporta la radio Bluetooth virtual del lado del móvil?

   a) esp-emu
   b) Bumble
   c) PlatformIO
   d) netsim, que arranca con el emulador Android

### Por qué hace falta Bumble en medio · Teoría

Parece que sobra una pieza: si el emulador ejecuta un ESP32, ¿por qué no habla Bluetooth directamente? Tres razones:

1. **esp-emu solo emula chips RISC-V**: ESP32-C3, C6, H2 y P4. **Ninguno tiene Bluetooth clásico**: todos son *BLE only*. El ESP32 «clásico», que sí lo tiene, no se puede emular.
2. **La app usa SPP**, el perfil de Bluetooth clásico de los módulos HC-05/HC-06 de Arduino: un socket con un **flujo de bytes en cada sentido**.
3. **Bumble hace de HC-05.** El firmware habla por su UART1; Bumble publica el servicio SPP en la radio virtual y **reenvía cada byte** a esa UART. Tu código no cambia.

Compara las dos topologías:

![Dos filas alineadas. Con hardware real: ESP32 unido por UART al módulo HC-05, que habla por Bluetooth con el móvil. En esta práctica: esp-emu unido por un socket TCP a Bumble, el HC-05 virtual, que habla por Bluetooth con el emulador Android y su radio netsim. HC-05 y Bumble están en la misma columna, resaltados: cumplen el mismo papel.](/img/pde/laboratorio/hardware-vs-practica.svg "Hardware real frente a la práctica: Bumble ocupa el lugar del HC-05")

La línea recta es un cable (o su equivalente, un socket TCP) y la ondulada es radio Bluetooth. La **topología es la misma**: solo cambia quién pone la radio.

> [!ANALOGIA]
> El HC-05 es un traductor simultáneo: el ESP32 le habla por cable en «UART» y él lo repite por radio en «Bluetooth». Bumble es el mismo traductor, pero en software.

> [!NOTA] Ampliación
> `demo-esp32` trae también un modo **BLE** (NimBLE/GATT), que sí podría hablar el chip emulado. En esta práctica usamos SPP.

> [!EXAMEN]
> «¿Por qué no se conecta la app directamente al ESP32-C3 emulado por Bluetooth clásico?» Porque el C3 (como C6, H2 y P4) es **BLE only**: no tiene Bluetooth clásico. Bumble pone el SPP.

**Preguntas**

**5.** ¿Cuál es la razón principal de que haga falta Bumble?

   a) esp-emu es demasiado lento para gestionar la radio
   b) Los chips que emula esp-emu son BLE only y la app usa SPP, que es Bluetooth clásico
   c) Android no permite conectar con emuladores sin un intermediario Python
   d) PlatformIO no sabe compilar código Bluetooth

**6.** ¿Qué chips puede emular esp-emu?

   a) ESP32-C3
   b) ESP32 clásico
   c) ESP32-C6
   d) ESP32-H2
   e) ESP32-P4

### La cadena completa en modo SPP · Teoría

Esta es la cadena de extremo a extremo:

![Cadena vertical de cinco piezas. Firmware src/spp/main.cpp, que habla por Serial1 (UART1) como si hubiera un HC-05 cableado. esp-emu, que ejecuta merged_flash.bin y sirve esa UART en TCP 127.0.0.1:5556; una flecha naranja indica que es el puente quien se conecta a esp-emu. Puente Bumble (bumble-rfcomm-bridge), que publica el servicio SPP «ESP32-EMU-SPP» en el canal 1. netsim, la radio Bluetooth virtual del emulador Android. BluetoothActivity, que abre un socket RFCOMM al dispositivo emparejado. A la derecha, el firmware y la Activity se marcan como «tú escribes» y las tres piezas intermedias como «solo se arranca, con los scripts».](/img/pde/laboratorio/cadena-spp.svg "La cadena SPP de extremo a extremo: tú escribes los extremos, lo de en medio solo se arranca")

### Direcciones y sentido de la conexión

- La UART1 del firmware se sirve en **`127.0.0.1:5556`**. Escribe siempre la IP, **nunca «localhost»**: `localhost` puede resolver a la dirección IPv6 (`::1`) y entonces no encuentra a esp-emu.
- **El puente RFCOMM se conecta a esp-emu, no al revés.** esp-emu es el servidor TCP; Bumble es el cliente.

### El orden de arranque

Como el puente necesita encontrar a esp-emu escuchando, el orden importa:

<div class="flow">
  <div><b>1</b><small>Emulador Android</small></div>
  <div><b>2</b><small>esp-emu</small></div>
  <div><b>3</b><small>Puente Bumble</small></div>
  <div><b>4</b><small>Emparejar</small></div>
  <div><b>5</b><small>App</small></div>
</div>

Los pasos 2 y 3 los hace `./run.sh spp`, que ya arranca esp-emu primero y luego Bumble.

> [!ANALOGIA]
> Es una cadena de teléfonos: no puedes llamar a alguien que todavía no ha enchufado el suyo. esp-emu tiene que estar «descolgado» (escuchando en el 5556) antes de que Bumble marque.

**Preguntas**

**7.** Ordena el arranque de la cadena.

   Ordena: Abrir la app · Emparejar ESP32-EMU-SPP · Emulador Android · Puente Bumble · esp-emu

**8.** ¿En qué puerto TCP sirve esp-emu la UART1 del firmware?

**9.** ¿Por qué los scripts usan `127.0.0.1` y nunca `localhost`?

   a) Porque `localhost` puede resolver a IPv6 y no encontrar a esp-emu
   b) Porque `localhost` solo funciona en Windows
   c) Porque `127.0.0.1` es más rápido de escribir
   d) Porque Bumble no acepta nombres de máquina por licencia

### Lo que recibes y lo que instalas · Práctica

Recibes **dos ZIP** del profesor e instalas **dos herramientas nuevas**.

### ZIP `demo-esp32`

| Fichero | Para qué |
| --- | --- |
| `platformio.ini` | Dos entornos: `esp32c3` (BLE) y **`esp32c3-spp`** (el nuestro) |
| `src/spp/main.cpp` | El firmware de partida |
| `build.sh` y `run.sh` | Compilar y arrancar la cadena |
| `tools/hc05.json` | Identidad del HC-05 virtual |
| `tools/verify_spp.py` | Prueba de la cadena **sin app** |

### ZIP `pde-plantilla-sdk36`

La plantilla de la asignatura (Compose + vistas clásicas). Sus versiones están fijadas: **no toques `gradle/`**. Aquí añadirás `BluetoothActivity`, su layout y los permisos en el manifest. El emulador **Pixel 8 API 36** se crea desde *Device Manager*.

### Lo que instalas tú

- **esp-emu**: binario oficial de Espressif (instalador o *release* de GitHub). Hito: `esp-emu --help` responde.
- **Bumble**: paquete Python en un **entorno virtual**, con **Python 3.10 o superior**:

```bash
# desde la carpeta que CONTIENE demo-esp32 (los scripts esperan ../.venv)
python3 -m venv .venv
source .venv/bin/activate
pip install "bumble[all]"
```

Ya tienes del laboratorio anterior VS Code + PlatformIO y Android Studio.

> [!CUIDADO] Rutas que esperan los scripts
> `build.sh` y `run.sh` buscan el venv de Bumble en **`../.venv`** (una carpeta por encima del proyecto) y PlatformIO en **`~/.platformio`**. Si lo pones en otro sitio, ajusta los scripts.

Las comillas en `"bumble[all]"` no son decorativas: en zsh los corchetes se interpretan como patrón de ficheros y sin comillas el comando puede fallar.

**Preguntas**

**10.** Creas el venv de Bumble dentro de `demo-esp32/.venv` y `run.sh` no encuentra Bumble. ¿Por qué?

   a) Bumble necesita instalarse con permisos de administrador
   b) Los scripts esperan el venv en `../.venv`, una carpeta por encima del proyecto
   c) El venv debe llamarse `bumble`, no `.venv`
   d) Bumble solo funciona instalado en `~/.platformio`

**11.** ¿Qué versión mínima de Python necesitas para Bumble?

   a) Python 2.7
   b) Python 3.6
   c) Python 3.10
   d) Python 3.13

**12.** ¿Cómo se llama el entorno de `platformio.ini` que usamos en esta práctica (el de SPP)?

### Los siete pasos, con hitos · Práctica

Los pasos 1 a 3 se hacen **una sola vez**; del 4 al 7 son el **ciclo de trabajo** de la práctica.

| # | Qué | Con qué | Hito: sabes que está cuando… |
| --- | --- | --- | --- |
| 1 | Instalar esp-emu y Bumble | Instalador de esp-emu · venv + `pip install "bumble[all]"` | `esp-emu --help` responde |
| 2 | Compilar el firmware SPP | VS Code + PlatformIO · `./build.sh esp32c3-spp` | Existe `merged_flash.bin` |
| 3 | Arrancar el emulador Android | Android Studio · Pixel 8 API 36 | `netsimd` está corriendo |
| 4 | Arrancar esp-emu y el puente | `./run.sh spp` (esp-emu primero, luego Bumble) | Consola: «firmware SPP listo» |
| 5 | Emparejar (solo una vez) | Ajustes → Bluetooth → *Pair new device* | «ESP32-EMU-SPP» emparejado |
| 6 | Crear `BluetoothActivity` | Layout, permisos, socket RFCOMM, hilo lector | El Spinner lista el dispositivo |
| 7 | Cerrar el bucle | Cambiar firmware y app; recompilar, reinstalar | Ticks y ecos en la app |

> [!IMPORTANTE] Regla
> Cada fila es un **hito verificable**. Si no puedes marcar la última columna, **no pases a la siguiente fila**.

¿Por qué tanta insistencia? Porque en una cadena de cinco piezas, un fallo en la fila 2 se manifiesta como «la app no conecta» en la fila 7, y entonces no sabes dónde mirar. Avanzar hito a hito es **aislar el fallo antes de que se esconda**.

> [!TRUCO]
> El emparejamiento (paso 5) se hace **una vez**. Si reinicias el puente más adelante, no vuelvas a emparejar: el dispositivo sigue en la lista de emparejados. Lo que puede pasar es otra cosa (el «enlace zombi»), que verás en el diagnóstico.

**Preguntas**

**13.** Ordena los siete pasos del laboratorio.

   Ordena: Arrancar el emulador Android · Arrancar esp-emu y el puente · Cerrar el bucle · Compilar el firmware SPP · Crear BluetoothActivity · Emparejar ESP32-EMU-SPP · Instalar esp-emu y Bumble

**14.** ¿Cuál es el hito del paso 4 (arrancar esp-emu y el puente)?

   a) Existe `merged_flash.bin`
   b) `netsimd` está corriendo
   c) El Spinner lista el dispositivo
   d) La consola muestra «firmware SPP listo»

### El firmware: Serial, Serial1 y merged_flash.bin · Práctica

Proyecto `demo-esp32`, entorno `esp32c3-spp`, placa `esp32-c3-devkitm-1`, framework Arduino, **sin librerías extra**.

### Qué tocar: `src/spp/main.cpp`

El firmware usa **dos puertos serie** con papeles distintos:

| Puerto | Para qué | Dónde lo ves |
| --- | --- | --- |
| `Serial` | **Logs** de depuración | Consola de esp-emu |
| `Serial1` (UART1, pines 4 y 5) | **Hablar con la app** | Bumble → Bluetooth → app |

El firmware lee líneas de `Serial1`, responde con eco y manda un tick cada 5 s. Un boceto orientativo (la configuración exacta de `Serial1` es la de la plantilla):

```cpp
uint32_t ultimoTick = 0;
uint32_t ecos = 0;
String linea;

void setup() {
  Serial.begin(115200);                       // logs → consola de esp-emu
  // Serial1.begin(...): baudios y pines 4/5 como vienen en la plantilla
  Serial.println("[APP] firmware SPP listo");
}

void loop() {
  if (millis() - ultimoTick >= 5000) {        // evento temporal: tick
    ultimoTick = millis();
    Serial1.printf("tick %lu s\n", (unsigned long) (millis() / 1000));
  }
  while (Serial1.available()) {               // evento externo: bytes
    char c = Serial1.read();
    if (c == '\n') {
      Serial.printf("[UART1] recibido: %s\n", linea.c_str());
      Serial1.printf("eco #%lu: %s\n", (unsigned long) ++ecos, linea.c_str());
      linea = "";
    } else if (c != '\r') {
      linea += c;
    }
  }
}
```

### Cómo compilar

```bash
./build.sh esp32c3-spp
```

El script hace `pio run` y después `esptool merge_bin`. **esp-emu quiere la imagen unificada `merged_flash.bin`, no `firmware.bin`**: necesita toda la flash montada (no solo tu aplicación), como si fuera el chip real recién grabado.

> [!IMPORTANTE] Regla del ciclo
> Cada cambio en el firmware = **recompilar y relanzar esp-emu**. La app **no** hay que reinstalarla.

> [!EXAMEN]
> Diferencia `Serial` / `Serial1` y `firmware.bin` / `merged_flash.bin`: son las dos confusiones más típicas de esta práctica.

**Preguntas**

**15.** Escribes `Serial.println("hola")` esperando verlo en la app, pero solo aparece en la consola de esp-emu. ¿Qué ha pasado?

   a) Bumble no está arrancado
   b) `Serial` es el puerto de logs; para la app hay que escribir en `Serial1`
   c) Hay que reinstalar la app
   d) Falta el permiso `BLUETOOTH_CONNECT`

**16.** ¿Cómo se llama la imagen unificada que carga esp-emu?

### Ejercicio: ¿qué tengo que relanzar? · Ejercicio

En el ciclo de trabajo (pasos 4 a 7) vas a cambiar cosas a ambos lados constantemente. Saber **qué relanzar** según qué has tocado te ahorra muchos minutos.

| Has cambiado… | Tienes que… | No hace falta… |
| --- | --- | --- |
| El firmware (`main.cpp`) | `./build.sh esp32c3-spp` y relanzar esp-emu (`./run.sh spp`) | Reinstalar la app |
| La Activity o el layout | Recompilar y reinstalar la app desde Android Studio | Recompilar el firmware |
| Nada, solo reiniciaste el puente | Puede que Android crea que sigue el enlace anterior (ver diagnóstico) | Volver a emparejar |

Piensa en la cadena como piezas independientes que se hablan por «tuberías» (TCP, Bluetooth). Cambiar una pieza solo obliga a reiniciar **esa** pieza y, como mucho, las que dependen de ella para conectarse.

<details><summary>Caso resuelto</summary>
<p>Cambias el periodo del tick de 5 s a 2 s. Recompilas con <code>./build.sh esp32c3-spp</code> y relanzas <code>./run.sh spp</code>. Como esp-emu arranca de nuevo, el contador de segundos del tick vuelve a empezar. La app sigue instalada; si se queda «colgada» tras el reinicio del puente, mira el caso del enlace zombi.</p>
</details>

**Preguntas**

**17.** Cambias el texto del tick en el firmware. ¿Qué tienes que hacer?

   a) Ejecutar `./build.sh esp32c3-spp`
   b) Relanzar esp-emu con `./run.sh spp`
   c) Reinstalar la app
   d) Volver a emparejar ESP32-EMU-SPP

**18.** Añades un botón nuevo a la Activity que envía el comando `reset`. ¿Qué recompilas?

   a) Solo el firmware
   b) El firmware y la app
   c) Nada: Android recarga el layout solo
   d) Solo la app, y la reinstalas

### Repaso: la cadena en una página · Repaso

> [!RESUMEN] La cadena SPP
> **Firmware** (`Serial1`, UART1) → **esp-emu** (`merged_flash.bin`, UART1 en `127.0.0.1:5556`) → **Bumble** (HC-05 virtual, servidor SPP «ESP32-EMU-SPP», canal 1) → **netsim** (radio del emulador) → **app** (socket RFCOMM).

| Idea | Recuerda |
| --- | --- |
| Objetivo | Tick cada 5 s y eco a cada mensaje, por Bluetooth clásico (SPP) |
| Regla de oro | Escribes los extremos (firmware y Activity); lo de en medio solo se arranca |
| ¿Por qué Bumble? | esp-emu emula C3/C6/H2/P4, todos BLE only; Bumble pone el SPP |
| Orden | Emulador Android → esp-emu → puente Bumble → emparejar → app |
| Direcciones | `127.0.0.1`, nunca `localhost`; el puente se conecta a esp-emu |
| Firmware | `Serial` = logs; `Serial1` (pines 4 y 5) = app |
| Imagen | esp-emu carga `merged_flash.bin`, no `firmware.bin` |
| Ciclo | Cambio de firmware → recompilar y relanzar esp-emu; la app no se reinstala |
| Hitos | Si no marcas el hito de una fila, no pases a la siguiente |

**Preguntas**

**19.** En modo SPP, ¿quién inicia la conexión TCP entre esp-emu y Bumble?

   a) esp-emu se conecta a Bumble
   b) Las dos piezas se conectan a netsim y este las une
   c) El puente Bumble se conecta a esp-emu
   d) La app Android abre la conexión TCP

**20.** Marca las afirmaciones **correctas** sobre el laboratorio.

   a) El ESP32-C3 emulado no tiene Bluetooth clásico
   b) Hay que emparejar el dispositivo cada vez que se relanza el puente
   c) `tools/verify_spp.py` permite probar la cadena sin la app
   d) Los scripts `build.sh` y `run.sh` vienen en el ZIP `demo-esp32`
   e) esp-emu carga directamente `firmware.bin`

**21.** Escribe el comando que arranca esp-emu y el puente Bumble en modo SPP.

### Solucionario

1. c) Un `tick N s` cada 5 segundos  
   El firmware manda un `tick` cada 5 s por su cuenta. El `eco #N` solo aparece como respuesta a un mensaje tuyo.
2. b) Bluetooth clásico con el perfil SPP, como un HC-05  
   Se usa Bluetooth clásico (SPP), el perfil de los módulos HC-05/HC-06. El modo BLE existe solo como ampliación.
3. a) El firmware `src/spp/main.cpp`; d) La `BluetoothActivity` en Kotlin  
   Escribes los dos extremos: firmware y Activity. esp-emu, Bumble y el emulador Android (con netsim) solo se arrancan con los scripts.
4. d) netsim, que arranca con el emulador Android  
   netsim es la radio Bluetooth virtual del emulador Android. Bumble publica el servicio SPP en esa radio, y esp-emu solo expone la UART por TCP.
5. b) Los chips que emula esp-emu son BLE only y la app usa SPP, que es Bluetooth clásico  
   C3, C6, H2 y P4 no tienen Bluetooth clásico. Bumble actúa como un HC-05 virtual que convierte la UART en un servicio SPP.
6. a) ESP32-C3; c) ESP32-C6; d) ESP32-H2; e) ESP32-P4  
   esp-emu emula los RISC-V C3, C6, H2 y P4. El ESP32 «clásico» no se puede emular.
7. 1. Emulador Android → 2. esp-emu → 3. Puente Bumble → 4. Emparejar ESP32-EMU-SPP → 5. Abrir la app  
   Emulador Android → esp-emu → puente Bumble → emparejar → app. esp-emu va antes que el puente porque es el puente quien se conecta a esp-emu.
8. `5556`  
   La UART1 queda en `127.0.0.1:5556`.
9. a) Porque `localhost` puede resolver a IPv6 y no encontrar a esp-emu  
   `localhost` puede resolverse a `::1` (IPv6). Con la IPv4 explícita no hay ambigüedad.
10. b) Los scripts esperan el venv en `../.venv`, una carpeta por encima del proyecto  
   Los scripts esperan `../.venv`. O lo mueves ahí o ajustas `build.sh` y `run.sh`.
11. c) Python 3.10  
   El material pide Python 3.10 o superior.
12. `esp32c3-spp`  
   `esp32c3-spp`. El otro entorno, `esp32c3`, es el modo BLE de ampliación.
13. 1. Instalar esp-emu y Bumble → 2. Compilar el firmware SPP → 3. Arrancar el emulador Android → 4. Arrancar esp-emu y el puente → 5. Emparejar ESP32-EMU-SPP → 6. Crear BluetoothActivity → 7. Cerrar el bucle  
   Instalar → compilar → emulador Android → esp-emu + puente → emparejar → Activity → cerrar el bucle.
14. d) La consola muestra «firmware SPP listo»  
   Al arrancar la cadena, la consola de esp-emu debe mostrar el mensaje del firmware. `merged_flash.bin` es el hito del paso 2, `netsimd` del 3 y el Spinner del 6.
15. b) `Serial` es el puerto de logs; para la app hay que escribir en `Serial1`  
   `Serial` va a la consola de esp-emu. Lo que viaja a la app es lo que se escribe en `Serial1` (UART1), que Bumble reenvía por SPP.
16. `merged_flash.bin`  
   `merged_flash.bin`, generada por `build.sh` con `esptool merge_bin`. No vale `firmware.bin` a secas.
17. a) Ejecutar `./build.sh esp32c3-spp`; b) Relanzar esp-emu con `./run.sh spp`  
   Cambio de firmware = recompilar y relanzar esp-emu. La app no se reinstala y el emparejamiento se conserva.
18. d) Solo la app, y la reinstalas  
   Has tocado la Activity, así que recompilas y reinstalas la app. Si además quieres que el firmware reaccione de forma especial a `reset`, eso sería otro cambio, ya en el firmware.
19. c) El puente Bumble se conecta a esp-emu  
   El puente RFCOMM se conecta a esp-emu, no al revés. Por eso esp-emu debe arrancar antes.
20. a) El ESP32-C3 emulado no tiene Bluetooth clásico; c) `tools/verify_spp.py` permite probar la cadena sin la app; d) Los scripts `build.sh` y `run.sh` vienen en el ZIP `demo-esp32`  
   El emparejamiento se hace una sola vez y esp-emu carga `merged_flash.bin`. El resto es correcto.
21. `./run.sh spp`  
   `./run.sh spp` arranca esp-emu primero y luego el puente Bumble.

## La BluetoothActivity, eventos externos y diagnóstico

_Permisos, dispositivos emparejados, socket RFCOMM con el UUID SPP en un hilo aparte, hilo lector con runOnUiThread y su relación con Observer. Cierra con Windows/WSL2, los tres casos de «no conecta» y la lista de llegada._

### Manifest y permiso BLUETOOTH_CONNECT · Práctica

La Activity nueva se llama `BluetoothActivity` y su interfaz es **XML con vistas clásicas**, como la de `ContadorVistas`. La novedad es que algunos eventos ya no vienen de la pantalla, sino **del socket**.

### Solo un permiso

```xml
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

- **`BLUETOOTH_CONNECT`**: necesario para hablar con dispositivos ya emparejados. Es un permiso peligroso, así que además de declararlo **se pide en tiempo de ejecución**.
- **Sin `BLUETOOTH_SCAN`**: no buscamos dispositivos nuevos; usamos **los ya emparejados** (paso 5 del laboratorio).

```kotlin
private val REQ_BT = 1

private fun pedirPermiso() {
    val concedido = ContextCompat.checkSelfPermission(
        this, Manifest.permission.BLUETOOTH_CONNECT
    ) == PackageManager.PERMISSION_GRANTED
    if (concedido) cargarEmparejados()
    else ActivityCompat.requestPermissions(
        this, arrayOf(Manifest.permission.BLUETOOTH_CONNECT), REQ_BT
    )
}

override fun onRequestPermissionsResult(
    requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == REQ_BT && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
        cargarEmparejados()
    }
}
```

Fíjate en que la respuesta del usuario al diálogo de permisos **también es un evento**: `onRequestPermissionsResult` es un callback que el sistema invoca cuando el usuario decide.

> [!NOTA]
> Menos permisos = menos diálogos para el usuario y menos superficie de error. Si no escaneas, no pidas `BLUETOOTH_SCAN`.

**Preguntas**

**1.** ¿Qué permiso declaras en el manifest y pides en tiempo de ejecución para usar los dispositivos emparejados?

**2.** ¿Por qué la práctica **no** pide `BLUETOOTH_SCAN`?

   a) Porque en API 36 ya no existe
   b) Porque el emulador concede todos los permisos automáticamente
   c) Porque no se buscan dispositivos: se usan los ya emparejados
   d) Porque Bumble hace el escaneo por la app

### Adaptador y dispositivos emparejados · Práctica

El adaptador Bluetooth se obtiene **a través de `BluetoothManager`**. Si está apagado, se pide al usuario que lo encienda con `ACTION_REQUEST_ENABLE`. Después, `bondedDevices` (los dispositivos emparejados) rellena un **Spinner**.

```kotlin
private var emparejados: List<BluetoothDevice> = emptyList()

@SuppressLint("MissingPermission") // ya comprobado en pedirPermiso()
private fun cargarEmparejados() {
    val manager = getSystemService(BluetoothManager::class.java)
    val adapter = manager.adapter ?: return          // sin Bluetooth
    if (!adapter.isEnabled) {
        startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
        return
    }
    emparejados = adapter.bondedDevices.toList()
    spinner.adapter = ArrayAdapter(
        this,
        android.R.layout.simple_spinner_dropdown_item,
        emparejados.map { it.name ?: it.address }
    )
}
```

Hito del paso 6: **el Spinner lista `ESP32-EMU-SPP`**. Si no aparece, el problema está antes: ¿emparejaste en *Ajustes → Bluetooth → Pair new device*?

> [!CUIDADO] Emparejado no es lo mismo que disponible
> `bondedDevices` devuelve lo que Android **recuerda** haber emparejado, no lo que está encendido ahora. Que `ESP32-EMU-SPP` salga en el Spinner **no garantiza** que esp-emu y el puente estén corriendo. Esto es la base del primer caso del diagnóstico.

<div class="flow">
  <div><b>Permiso</b><small>BLUETOOTH_CONNECT</small></div>
  <div><b>Manager</b><small>BluetoothManager.adapter</small></div>
  <div><b>¿Encendido?</b><small>si no, ACTION_REQUEST_ENABLE</small></div>
  <div><b>bondedDevices</b><small>lista de emparejados</small></div>
  <div><b>Spinner</b><small>el usuario elige</small></div>
</div>

**Preguntas**

**3.** ¿Qué propiedad del adaptador devuelve los dispositivos ya emparejados?

**4.** `ESP32-EMU-SPP` aparece en el Spinner. ¿Qué puedes concluir con seguridad?

   a) Que esp-emu está ejecutando el firmware
   b) Que el puente Bumble está conectado a esp-emu
   c) Que el socket RFCOMM ya está abierto
   d) Que el dispositivo se emparejó en algún momento

### Conectar: socket RFCOMM en un Thread · Práctica

Para hablar SPP se abre un **socket RFCOMM** con el **UUID estándar del perfil SPP**, que empieza por `00001101-`:

```kotlin
private val SPP_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
private var socket: BluetoothSocket? = null

@SuppressLint("MissingPermission")
private fun conectar(device: BluetoothDevice) {
    estado = "Conectando con ${device.name}…"
    actualizarVista()
    Thread {
        try {
            val s = device.createRfcommSocketToServiceRecord(SPP_UUID)
            s.connect()                       // BLOQUEA hasta conectar o fallar
            socket = s
            runOnUiThread { estado = "Conectado"; actualizarVista() }
            arrancarLector(s)
        } catch (e: IOException) {
            runOnUiThread { estado = "Error de conexión: ${e.message}"; actualizarVista() }
        }
    }.start()
}
```

### Dos reglas

1. **`connect()` dentro de un `Thread`, nunca en el hilo principal.** Es una llamada bloqueante: puede tardar segundos o no volver hasta el *timeout*. En el hilo principal congelaría la interfaz (y Android acabaría mostrando «la app no responde»).
2. Las vistas solo se tocan desde el hilo principal: por eso el resultado vuelve con `runOnUiThread`.

> [!CUIDADO] Trampa: `cancelDiscovery()`
> Muchos ejemplos de Internet llaman a `adapter.cancelDiscovery()` antes de `connect()`. **No lo hagas aquí**: ese método exige `BLUETOOTH_SCAN`, que no hemos pedido, y la app **casca** con una excepción de seguridad.

> [!EXAMEN]
> «¿Por qué `connect()` va en otro hilo?» Porque bloquea. El hilo principal es el que procesa los eventos de la interfaz; si se queda esperando, no se atiende ningún clic ni se repinta nada.

**Preguntas**

**5.** Un compañero llama a `socket.connect()` directamente en el `onClick` del botón Conectar. ¿Qué ocurrirá?

   a) Nada grave: `connect()` es asíncrono y vuelve enseguida
   b) La interfaz se congela mientras `connect()` bloquea el hilo principal
   c) Android lanza la conexión en otro hilo automáticamente
   d) El emparejamiento se pierde

**6.** Añades `adapter.cancelDiscovery()` antes de `connect()` porque lo viste en un tutorial y la app se cierra al conectar. ¿Por qué?

   a) `cancelDiscovery()` exige `BLUETOOTH_SCAN`, que la app no tiene
   b) `cancelDiscovery()` desempareja el dispositivo
   c) `cancelDiscovery()` cierra el socket antes de abrirlo
   d) `cancelDiscovery()` solo funciona con BLE

**7.** ¿Por qué grupo de dígitos empieza el UUID estándar del perfil SPP?

### Hilo lector, runOnUiThread y enviar · Práctica

Una vez conectado, los bytes del ESP32 **llegan cuando quieren**: un tick cada 5 s, un eco tras cada mensaje. Para recibirlos hace falta un **hilo lector** que espere sobre `inputStream` y convierta **cada línea en un evento de interfaz**:

```kotlin
private val log = mutableListOf<String>()

private fun arrancarLector(s: BluetoothSocket) {
    Thread {
        try {
            val lector = s.inputStream.bufferedReader()
            while (true) {
                val linea = lector.readLine() ?: break   // bloquea hasta que llega una línea
                runOnUiThread {
                    log.add("<< $linea")
                    actualizarVista()
                }
            }
        } catch (e: IOException) {
            runOnUiThread { estado = "Desconectado"; actualizarVista() }
        }
    }.start()
}
```

**Enviar** es lo contrario: escribir en `outputStream`. Añadimos el salto de línea porque el firmware lee **líneas**:

```kotlin
private fun enviar(texto: String) {
    val s = socket ?: return
    Thread { s.outputStream.write("$texto\n".toByteArray()) }.start()
    log.add(">> $texto")
    actualizarVista()
}
```

### Una sola función que pinta

Todos los eventos acaban en el mismo sitio:

```kotlin
private fun actualizarVista() {
    tvEstado.text = estado
    tvLog.text = log.joinToString("\n")
}
```

> [!TRUCO]
> `readLine()` es bloqueante, igual que `connect()`. Por eso el lector vive en su propio hilo: el principal queda libre para atender los clics de «Enviar».

| Acción | Hilo | Mecanismo |
| --- | --- | --- |
| `connect()` | Secundario | `Thread { }` |
| Leer líneas | Secundario (lector) | `inputStream` + `readLine()` |
| Tocar vistas | Principal | `runOnUiThread { }` |
| Enviar | Escribe en `outputStream` | `"texto\n"` |

**Preguntas**

**8.** ¿Qué función usa el hilo lector para que la línea recibida se pinte en pantalla?

   a) `Thread.sleep()`
   b) `runOnUiThread { }`
   c) `socket.connect()`
   d) `setContentView()`

**9.** Envías `hola` sin salto de línea y el ESP32 nunca responde con el eco. ¿Por qué?

   a) Porque el firmware lee por líneas y espera el `\n` para procesar el mensaje
   b) Porque `outputStream` solo admite números
   c) Porque falta el permiso `BLUETOOTH_SCAN`
   d) Porque el eco solo se manda cada 5 segundos

### Eventos que no vienen del dedo · Teoría

Esto es lo que de verdad enseña la práctica: **eventos que llegan por un socket, en otro hilo**.

<div class="cards">
  <div class="card"><h4>Evento de usuario</h4><p><code>onClick</code>, <code>onCheckedChanged</code>. Llegan <b>ya en el hilo principal</b>, igual que en el contador de pulsaciones.</p></div>
  <div class="card"><h4>Evento externo</h4><p>Bytes que llegan cuando quieren, <b>en otro hilo</b>. Hay que traerlos al hilo de la interfaz con <code>runOnUiThread</code> para repintar.</p></div>
  <div class="card"><h4>Misma regla</h4><p>Un solo estado y una función que vuelca el estado en las vistas: <code>actualizarVista()</code>. Los dos tipos de evento la llaman.</p></div>
</div>

![Dos carriles. Arriba, el hilo lector (secundario): inputStream con los bytes del socket, readLine() que bloquea hasta tener una línea, y la línea «tick 375 s», que es un evento. Una flecha naranja, runOnUiThread, la baja al carril de abajo, el hilo principal (UI). Allí, el clic en «Enviar» ya llega directamente; ambos caminos pasan por log.add, el único estado, y terminan en actualizarVista(), que pinta tvEstado y tvLog.](/img/pde/laboratorio/hilos-activity.svg "El clic ya está en el hilo principal; la línea del socket necesita runOnUiThread para llegar")

### Enlace con la Unidad 2: Observer con un hilo en medio

<div class="flow">
  <div><b>Hilo lector</b><small>productor: genera un evento por línea</small></div>
  <div><b>runOnUiThread</b><small>puente al hilo principal</small></div>
  <div><b>Activity</b><small>suscriptor: actualiza el estado</small></div>
  <div><b>actualizarVista()</b><small>vuelca el estado en las vistas</small></div>
</div>

Es el **Observer de la Clase 3** con una diferencia: el sujeto (el hilo lector) no vive en el mismo hilo que el observador (la Activity). `runOnUiThread` hace de **puente**: no llama al observador directamente, sino que **encola** el aviso en el hilo principal, que lo procesará en su bucle de eventos cuando le toque.

> [!ANALOGIA]
> El hilo lector es un mensajero que trae cartas a cualquier hora. No puede entrar en el despacho (las vistas) mientras el jefe (hilo principal) trabaja: deja la carta en la bandeja de entrada (`runOnUiThread`) y el jefe la abre cuando termina lo que estaba haciendo.

> [!EXAMEN]
> Identifica los roles: **productor** = hilo lector, **suscriptor** = Activity, **puente** = `runOnUiThread`, **única función de pintado** = `actualizarVista()`. Y razona por qué un `onClick` no necesita puente y un byte del socket sí.

**Preguntas**

**10.** Ordena el viaje de un `tick` desde que llega por el socket hasta que lo ves.

   Ordena: El hilo lector completa la línea con `readLine()` · La Activity añade la línea al estado · Los bytes llegan al `inputStream` del socket · `actualizarVista()` vuelca el estado en las vistas · `runOnUiThread` encola el aviso en el hilo principal

**11.** ¿Por qué un `onClick` **no** necesita `runOnUiThread` y una línea del socket sí?

   a) Porque `onClick` es más rápido
   b) Porque el `onClick` llega ya en el hilo principal; la línea del socket llega en el hilo lector
   c) Porque los eventos de Bluetooth tienen más prioridad
   d) Porque `runOnUiThread` solo funciona con datos de red

**12.** En la analogía con Observer, marca las asociaciones **correctas**.

   a) Hilo lector → productor (sujeto)
   b) Activity → suscriptor (observador)
   c) `runOnUiThread` → suscriptor
   d) `runOnUiThread` → puente al hilo principal
   e) `actualizarVista()` → productor

### Si trabajas en Windows: WSL2 · Práctica

**esp-emu solo tiene binarios para Linux y macOS.** En Windows, el chip emulado corre en **WSL2** (Ubuntu). El resto se queda en Windows:

| Pieza | Dónde corre | Nota |
| --- | --- | --- |
| VS Code + PlatformIO (compilar) | Windows | Igual que en el lab anterior |
| esp-emu (ejecutar el firmware) | **WSL2 (Ubuntu)** | Instalador oficial dentro de WSL; el `.bin` se lee desde `/mnt/c/…` |
| Bumble (HC-05 virtual) | Windows (Python) | Llega a esp-emu por localhost: WSL2 reenvía los puertos |
| Android Studio + emulador | Windows | netsim arranca con el emulador, sin cambios |
| `build.sh` y `run.sh` | Git Bash o WSL | Son scripts bash; en PowerShell no funcionan tal cual |

`/mnt/c/` es cómo ve WSL2 la unidad `C:\` de Windows: así esp-emu, dentro de Linux, puede leer el `merged_flash.bin` que compilaste en Windows.

<div class="flow">
  <div><b>Windows</b><small>PlatformIO compila merged_flash.bin</small></div>
  <div><b>WSL2</b><small>esp-emu lo lee de /mnt/c/… y abre :5556</small></div>
  <div><b>Windows</b><small>Bumble llega a :5556 por el reenvío de puertos</small></div>
  <div><b>Windows</b><small>emulador Android + netsim</small></div>
</div>

> [!CUIDADO] Ruta sin validar
> Según el propio material, **esta ruta todavía no se ha probado en el laboratorio**: la cadena completa solo está verificada en **macOS**. Queda pendiente confirmar antes de la sesión si se usará WSL2 o una máquina virtual Linux completa. Si estás en Windows, avanza hito a hito y apunta dónde te atascas.

> [!TRUCO]
> Si `./build.sh` te da errores raros en Windows, comprueba que no lo estás lanzando desde PowerShell. Abre Git Bash (o WSL) y repite.

**Preguntas**

**13.** En Windows, ¿dónde se ejecuta esp-emu?

   a) En PowerShell, como un .exe
   b) Dentro del emulador Android
   c) En WSL2 (Ubuntu), porque solo hay binarios para Linux y macOS
   d) En Git Bash, que emula Linux

**14.** Lanzas `./run.sh spp` desde PowerShell y no funciona. ¿Qué haces?

   a) Reescribirlo en PowerShell
   b) Ejecutarlo como administrador
   c) Instalar esp-emu para Windows
   d) Lanzarlo desde Git Bash o WSL, porque es un script bash

### Ejercicio: cuando no conecta, tres casos · Ejercicio

Tres síntomas típicos y sus tres causas. Lee las causas y resuelve los casos de abajo emparejando síntoma y causa.

<div class="cards">
  <div class="card"><h4>A · No hay nadie al otro lado</h4><p>esp-emu o el puente no están corriendo. El dispositivo sigue emparejado y por eso aparece en el Spinner. Arreglo: lanza <code>./run.sh spp</code> y reintenta.</p></div>
  <div class="card"><h4>B · Enlace zombi</h4><p>Reiniciaste el puente y Android cree que sigue el enlace anterior. Arreglo: reinicia el Bluetooth del emulador <b>sin perder el emparejamiento</b>.</p></div>
  <div class="card"><h4>C · Orden o dirección equivocados</h4><p>En modo SPP esp-emu va antes que el puente, y siempre <code>127.0.0.1</code>, no <code>localhost</code> (que resuelve a IPv6).</p></div>
</div>

Para el enlace zombi:

```bash
adb shell cmd bluetooth_manager disable
adb shell cmd bluetooth_manager enable
```

> [!TRUCO] Cómo razonar
> - Si el error sale **en la app** al conectar → mira primero si la cadena está levantada.
> - Si la app se queda **esperando sin error** → sospecha de un estado antiguo en Android.
> - Si el error sale **al arrancar los scripts** → la app ni ha entrado en juego: es orden o dirección.

<details><summary>¿Por qué el caso A es tan engañoso?</summary>
<p>Porque el Spinner se rellena con <code>bondedDevices</code>, que es memoria de Android, no estado real. Ver «ESP32-EMU-SPP» en la lista da sensación de que todo está en marcha, pero puede que no haya nada escuchando.</p>
</details>

**Preguntas**

**15.** Pulsas Conectar y aparece `Error de conexión: read failed, socket might closed or timeout`. ¿Causa más probable?

   a) A · No hay nadie al otro lado: esp-emu o el puente no están corriendo
   b) B · Enlace zombi
   c) C · Orden o dirección equivocados
   d) Falta el permiso `BLUETOOTH_SCAN`

**16.** Acabas de reiniciar el puente. La app muestra «Conectando con ESP32-EMU-SPP…» y no pasa nada, sin error. ¿Qué haces?

   a) Desemparejar y volver a emparejar el dispositivo
   b) Reiniciar el Bluetooth del emulador con `adb shell cmd bluetooth_manager disable` / `enable`
   c) Cambiar `127.0.0.1` por `localhost`
   d) Añadir `cancelDiscovery()` antes de `connect()`

**17.** Al ejecutar los scripts sale `Connection refused` al arrancar esp-emu o el puente. ¿Causa?

   a) A · No hay nadie al otro lado
   b) B · Enlace zombi
   c) Falta el permiso `BLUETOOTH_CONNECT`
   d) C · Orden o dirección equivocados: el puente arrancó antes que esp-emu, o se usó `localhost`

### Cómo saber que has llegado · Práctica

Cuatro comprobaciones. Las dos primeras demuestran que la cadena funciona; **las dos últimas cierran el bucle**, porque demuestran que controlas los dos extremos.

- [ ] La consola de esp-emu muestra `[APP] firmware SPP listo` y, al enviar desde la app, `[UART1] recibido: …`.
- [ ] La app recibe los `tick N s` **sin tocar nada** y un `eco #N` por cada mensaje enviado.
- [ ] Has **cambiado el firmware** (el texto del tick, el periodo) y se ve en la app tras recompilar.
- [ ] Has **cambiado la Activity** (un botón nuevo, un comando) y el firmware reacciona.

### Prueba sin app: `verify_spp.py`

Si la app no conecta y no sabes si el problema está en tu Activity o en la cadena, usa el script del ZIP:

```bash
# desde demo-esp32, con el venv de Bumble activado
python tools/verify_spp.py
```

Recorre **el mismo camino** que la app (TCP → RFCOMM → puente → UART1) y debe terminar en **«OK»**.

| verify_spp.py | La app | Dónde está el problema |
| --- | --- | --- |
| OK | Conecta | En ningún sitio |
| OK | No conecta | En tu **Activity** (permisos, UUID, hilos…) |
| Falla | No conecta | En la **cadena** (esp-emu, puente, orden, dirección) |

> [!TRUCO]
> Esta tabla es la técnica de **dividir el problema por la mitad**: una prueba que cubre la mitad del camino te dice en qué mitad buscar.

**Preguntas**

**18.** `tools/verify_spp.py` termina en «OK», pero tu app no conecta. ¿Dónde buscas?

   a) En esp-emu: hay que reinstalarlo
   b) En el puente Bumble: hay que reiniciarlo
   c) En tu Activity: permisos, UUID, hilos…
   d) En el firmware: hay que recompilarlo

**19.** ¿Qué comprobaciones **cierran el bucle** según el material?

   a) Cambiar el firmware y ver el cambio en la app
   b) Ver `[APP] firmware SPP listo` en la consola
   c) Cambiar la Activity y ver que el firmware reacciona
   d) Ver los ticks en la app

### Repaso: la Activity y el diagnóstico · Repaso

> [!RESUMEN] BluetoothActivity en seis líneas
> 1. Manifest: `BLUETOOTH_CONNECT`, pedido en tiempo de ejecución; **sin** `BLUETOOTH_SCAN`.
> 2. `BluetoothManager` → adaptador; si está apagado, `ACTION_REQUEST_ENABLE`.
> 3. `bondedDevices` → Spinner.
> 4. `createRfcommSocketToServiceRecord` con el UUID SPP `00001101-…`; `connect()` en un `Thread`. **Nunca** `cancelDiscovery()`.
> 5. Hilo lector sobre `inputStream` → `runOnUiThread` → `actualizarVista()`. Enviar = escribir en `outputStream`.
> 6. Productor (hilo lector) → puente (`runOnUiThread`) → suscriptor (Activity): Observer con un hilo en medio.

| Síntoma | Causa | Arreglo |
| --- | --- | --- |
| `read failed, socket might closed or timeout` | Nadie al otro lado | `./run.sh spp` y reintentar |
| «Conectando…» y nada | Enlace zombi | `adb shell cmd bluetooth_manager disable` / `enable` |
| `Connection refused` al arrancar | Orden o dirección | esp-emu antes que el puente; `127.0.0.1` |

**Windows:** esp-emu en WSL2; el resto en Windows; scripts en Git Bash o WSL. Ruta aún sin validar.

**Preguntas**

**20.** ¿Qué operaciones deben hacerse **fuera** del hilo principal?

   a) `socket.connect()`
   b) `lector.readLine()` en bucle
   c) `tvLog.text = …`
   d) `spinner.adapter = …`

**21.** ¿Qué herramienta de línea de comandos usas para reiniciar el Bluetooth del emulador Android en el caso del enlace zombi?

**22.** Pregunta de examen: ¿qué patrón de la Unidad 2 reconoces en el hilo lector + `runOnUiThread` + Activity?

   a) Singleton
   b) Observer, con un hilo en medio
   c) Factory Method
   d) MVC sin controlador

### Solucionario

1. `BLUETOOTH_CONNECT`  
   `BLUETOOTH_CONNECT`. No hace falta `BLUETOOTH_SCAN` porque no buscamos dispositivos.
2. c) Porque no se buscan dispositivos: se usan los ya emparejados  
   La app solo lista `bondedDevices` (emparejados). Sin búsqueda no hace falta `BLUETOOTH_SCAN`.
3. `bondedDevices`  
   `adapter.bondedDevices` (en Java, `getBondedDevices()`) devuelve el conjunto de dispositivos emparejados.
4. d) Que el dispositivo se emparejó en algún momento  
   `bondedDevices` refleja el emparejamiento guardado, no el estado actual de la cadena.
5. b) La interfaz se congela mientras `connect()` bloquea el hilo principal  
   `connect()` es bloqueante. En el hilo principal impide procesar eventos de interfaz: la app se congela.
6. a) `cancelDiscovery()` exige `BLUETOOTH_SCAN`, que la app no tiene  
   Es la trampa señalada en el material: `cancelDiscovery()` requiere `BLUETOOTH_SCAN` y, sin él, la app casca.
7. `00001101`  
   El UUID SPP es `00001101-0000-1000-8000-00805F9B34FB`.
8. b) `runOnUiThread { }`  
   `runOnUiThread` encola el bloque en el hilo principal, el único que puede tocar las vistas.
9. a) Porque el firmware lee por líneas y espera el `\n` para procesar el mensaje  
   El firmware acumula caracteres hasta recibir un fin de línea; sin `\n` el mensaje nunca se da por completo.
10. 1. Los bytes llegan al `inputStream` del socket → 2. El hilo lector completa la línea con `readLine()` → 3. `runOnUiThread` encola el aviso en el hilo principal → 4. La Activity añade la línea al estado → 5. `actualizarVista()` vuelca el estado en las vistas  
   Socket → hilo lector (productor) → `runOnUiThread` (puente) → Activity (suscriptor) actualiza el estado → `actualizarVista()`.
11. b) Porque el `onClick` llega ya en el hilo principal; la línea del socket llega en el hilo lector  
   Los eventos de usuario se entregan en el hilo principal. Los del socket aparecen en otro hilo y hay que trasladarlos para tocar las vistas.
12. a) Hilo lector → productor (sujeto); b) Activity → suscriptor (observador); d) `runOnUiThread` → puente al hilo principal  
   Productor = hilo lector, suscriptor = Activity, puente = `runOnUiThread`. `actualizarVista()` es la función que vuelca el estado.
13. c) En WSL2 (Ubuntu), porque solo hay binarios para Linux y macOS  
   esp-emu no tiene binario para Windows: se instala en WSL2 y lee el `.bin` desde `/mnt/c/…`.
14. d) Lanzarlo desde Git Bash o WSL, porque es un script bash  
   `build.sh` y `run.sh` son scripts bash: funcionan en Git Bash o en WSL, no en PowerShell tal cual.
15. a) A · No hay nadie al otro lado: esp-emu o el puente no están corriendo  
   El dispositivo sale en el Spinner porque está emparejado, pero no hay nadie escuchando. Solución: `./run.sh spp` y reintentar.
16. b) Reiniciar el Bluetooth del emulador con `adb shell cmd bluetooth_manager disable` / `enable`  
   Es el enlace zombi: Android cree que sigue el enlace anterior. Se reinicia el Bluetooth del emulador sin perder el emparejamiento. `cancelDiscovery()` haría cascar la app.
17. d) C · Orden o dirección equivocados: el puente arrancó antes que esp-emu, o se usó `localhost`  
   El puente se conecta a esp-emu; si esp-emu no escucha aún, o la dirección resuelve a IPv6, la conexión se rechaza. Los permisos de Android no intervienen al arrancar los scripts.
18. c) En tu Activity: permisos, UUID, hilos…  
   Si el script llega hasta la UART1, la cadena está bien. Lo que falla es lo único que el script no usa: tu Activity.
19. a) Cambiar el firmware y ver el cambio en la app; c) Cambiar la Activity y ver que el firmware reacciona  
   Las dos últimas comprobaciones exigen modificar cada extremo y ver el efecto al otro lado.
20. a) `socket.connect()`; b) `lector.readLine()` en bucle  
   `connect()` y `readLine()` bloquean: van en hilos secundarios. Tocar vistas (`tvLog`, `spinner`) debe hacerse en el hilo principal.
21. `adb`  
   `adb shell cmd bluetooth_manager disable` y luego `enable`: reinicia el Bluetooth sin perder el emparejamiento.
22. b) Observer, con un hilo en medio  
   El hilo lector produce eventos, la Activity los observa y `runOnUiThread` hace de puente entre hilos.
