Construyendo una app de radios LATAM (9): retrospectiva honesta y qué viene en v2

Publicado el 03/06/2026

El proyecto, en una frase

Una app móvil Android (y PWA) para escuchar radios de Latinoamérica. Construida en solitario, con Expo y React Native, sobre un backend propio que sirve de proxy a Radio Browser. Aproximadamente un mes de trabajo intermitente, dos commits significativos: uno de bootstrap y otro de remodelación completa de arquitectura.

Lo que salió bien

1. Expo + React Native era la elección correcta para mí

Me ahorró pelearme con Gradle, autolinking, certificados y firmas. El hot reload sigue siendo lo que más me hace querer programar de noche. La capa de Expo encima de React Native quita justo la fricción que más cansa.

Si la app fuera para iOS también, lo confirmo doblemente. Para Android-only, sigue siendo una ganancia, aunque el coste de oportunidad frente a Kotlin nativo es menor de lo que parece desde fuera.

2. Un backend propio en el medio

Probablemente la mejor decisión técnica que tomé. Me protege de:

  • Cambios de esquema en Radio Browser.
  • Mirrors caídos.
  • CORS y limitaciones del cliente.
  • Tener que actualizar la app por cosas que pasan en el servidor.

Coste: 5€/mes y un repo más que mantener. Beneficio: tranquilidad estructural.

3. NativeWind

Ningún arrepentimiento. Itero visualmente cuatro veces más rápido que con StyleSheet.create. Si vienes de Tailwind, la curva es de cero. Si no, una semana y vuelve.

4. Separar contextos

Partir ContextProvider en FavoritesContext y PlayerContext fue invisible para el usuario y enorme para mí. Menos rerenders, código más legible, tests posibles. Pequeña inversión, gran retorno.

5. Los patrones de control de concurrencia

tokenRef, AbortController, active flag, cola de operaciones. Una vez los tienes, los reutilizas en cualquier sitio con asincronía. Es de esas herramientas que pasan a ser parte de tu vocabulario.

6. Honestidad con el usuario sobre las limitaciones

Decidí desde temprano que streams http en Android se rechazan con un mensaje claro, no se "intentan" y fallan silenciosamente. Que mixed content en web también se rechaza explícitamente. Que el usuario sepa qué pasa es siempre mejor que un spinner eterno.

Lo que no salió tan bien

1. Demasiado código en PlayerContext desde el día uno

El contexto del reproductor tiene 300 líneas. Sí, la complejidad está justificada (token + cola + dos backends + estados + cleanup), pero leerlo de un tirón cuesta. Lo que debería hacer es:

  • Extraer el estado puro a una pequeña máquina (xstate o useReducer con tipos finos).
  • Mover resolveStreamUrl y createHandle a sus propios módulos.
  • Dejar el Context solo como pegamento entre la máquina y React.

No lo he hecho aún porque "funciona", pero el coste de mantenimiento crecerá si no lo hago.

2. El README estaba desactualizado

Después de la remodelación, el README.md seguía hablando de CountryPicker, StationItem, ContextProvider... componentes que ya no existían. Es la deuda más fácil de pagar y la que más a menudo se pospone. Lección aprendida: cada PR que cambia estructura debe actualizar el README.

3. Cero tests

Lo que tengo: typecheck y eslint. Lo que no tengo: ni un solo test. Para una app personal en v1, asumible. Para v2, ya no — voy a meter al menos tests sobre:

  • normalizeStation (entrada → salida).
  • sanitize de favoritos.
  • resolveStreamUrl (las reglas de http/https/Android/web).
  • El reducer/máquina del Player, cuando lo extraiga.

Lo no testeable directamente (la UI, los streams reales) seguirá siendo manual, pero la lógica pura debería estar cubierta.

4. No medí performance

No tengo idea de cuánto tarda en arrancar la app, ni cuánto pesa el bundle web, ni si la lista de emisoras hace 60 fps al hacer scroll en gama media. Para una v1 sirve "se siente bien", pero antes de publicar quiero números.

Lo siguiente que voy a meter:

  • expo-dev-launcher con perfil de release para medir startup.
  • npx expo export --platform web y mirar el tamaño del bundle.
  • react-native-performance o el inspector de Flipper para frames durante el scroll.

5. Accesibilidad incompleta

Tengo accessibilityRole y accessibilityLabel en lo crítico. Lo que no tengo:

  • Tamaño dinámico de fuente. Si el usuario tiene texto grande del sistema, mis layouts se rompen.
  • Contraste verificado. Eyeballeé los colores pero no pasé un check de WCAG.
  • Soporte de navegación por teclado en web. Hoy es regular.

Es deuda real, no decorativa.

Lo que dejé fuera a propósito (y no me arrepiento)

  • iOS. Mi público objetivo es Android. Añadir iOS es duplicar el esfuerzo de testing y certificados sin ROI claro hoy.
  • Login y usuarios. No hay cuentas. Los favoritos viven en el dispositivo. Cero backend para auth = cero superficie de ataque y cero coste de mantenimiento.
  • Sincronización cross-device. Consecuencia de no tener cuentas. Si cambias de móvil, pierdes favoritos. Asumido.
  • Anuncios y monetización. No hoy.
  • Métricas y analítica. No quería decidir aún qué herramienta usar y odio integrarlas a medias. Cuando la meta, será una sola, y será respetuosa con la privacidad.
  • Tests E2E con Detox o Maestro. Demasiado para una v1 personal.

Cada uno de estos podría justificarse mañana. Hoy, no estaban en el camino crítico.

Lo que viene en v2

Por orden de impacto percibido:

1. Notificación rica con controles del lockscreen

El feature que más me piden mentalmente cuando uso la app: que al bloquear el móvil aparezca la emisora con play/pause y un stop. Implica:

  • Migrar de expo-av a expo-audio o react-native-track-player.
  • Configurar MediaSession con metadatos.
  • Probar en varios dispositivos (Samsung, Pixel, Xiaomi tienen comportamientos distintos).

Es el proyecto técnico más sustancial pendiente.

2. Sleep timer

"Dormir con la radio puesta y que pare en 30 minutos". Un timeout sobre stop(). Una tarde de trabajo, mucho valor de uso real.

3. Histórico de últimas escuchadas

Lista de las últimas N emisoras que has reproducido, persistida igual que los favoritos. Pequeño, útil, casi gratis con los patrones que ya tengo.

4. Búsqueda combinada con filtros visibles

Hoy el buscador es por nombre, y los filtros (país, género) están separados. Quiero un "buscar dentro de los actuales filtros" más visible, con chips que se quitan al pulsarlos.

5. Caché in-memory con React Query

Lo dije en los capítulos 2 y 8: el día que useStationSearch empiece a oler a complejo, lo cambio por useQuery. v2 parece un buen momento.

6. Tests para la lógica pura

Mínimo:

  • normalizeStation (50 inputs raros → 50 outputs correctos).
  • sanitize favoritos (basura → lista limpia).
  • resolveStreamUrl (tabla de combinaciones plataforma × protocolo).

7. CI

GitHub Actions con typecheck, lint y los tests anteriores en cada push. Una mañana.

Las cinco lecciones que me llevo

Si esta serie le sirve a alguien que esté empezando un proyecto parecido, esto es lo que sintetizaría:

  1. Elige el stack por tus hábitos, no por hype. El mejor framework es el que vas a usar el sábado.
  2. Mete un backend en medio aunque parezca sobreingeniería. Te compra fiabilidad por casi nada.
  3. Normaliza en el borde, no en la vista. Tu UI debe poder asumir datos limpios.
  4. Un Context = una preocupación. Si dudas, parte.
  5. Sé honesto con el usuario sobre lo que no funciona. Un mensaje claro vale más que un spinner eterno.

Y una bonus: no construyas para hipótesis. Construye lo que vas a usar hoy, deja los huecos limpios para mañana. La app respira mejor y tú duermes mejor.

Cierre

Esta era la última entrada de la serie. Si has llegado hasta aquí, gracias por leer con paciencia un viaje a veces sinuoso. La app no está terminada —ninguna app lo está nunca— pero está en un sitio donde la disfruto, donde puedo iterar, y donde el camino a la v2 es claro.

El código vive en mi repo (privado por ahora; cuando estabilice v2 publicaré). El backend está en latam_radios_back. Si tienes preguntas o quieres comentar alguna decisión, me encuentras en mi blog.

Hasta la próxima serie.