PlisPlasNotas de taller
enunplisplas.com Hablemos
PlisPlasBlog
Hablemos

Mi app funciona en debug y falla en release: 8 causas y cómo cazarlas antes que tus usuarios

Equipo PlisPlas8 min de lectura

En tu móvil, con el cable puesto, todo va fino. Subes la versión a Google Play y llegan los cierres al abrir, el login que no entra o la notificación que nunca suena. No es magia negra: la compilación de release es otra app, y tiene sus propios fallos.

Cuando ejecutas flutter run estás en modo debug: sin optimizar, firmado con una clave de desarrollo, con permisos extra para que las herramientas hablen con la app y con las aserciones activas. La compilación de release cambia todo eso a la vez. Por eso hay fallos que no existen en debug ni en los tests y aparecen el día que la app llega a la tienda.

Comparativa · debug vs release
Aspecto
debugflutter run
releasela que llega a la tienda
Optimización
debug: Sin optimizarrelease: R8 en modo completo: elimina lo que cree que nadie usa y renombra el resto
Firma
debug: Clave de depuración del SDKrelease: Play App Signing: Google vuelve a firmar lo que subes
Permiso de internet
debug: INTERNET en el manifiesto de debugrelease: Solo si lo declara el manifiesto principal
assert(...)
debug: Se ejecutarelease: Desactivado: no corre nada de lo que hay dentro
Recursos
debug: Todos en el APKrelease: shrinkResources quita los que no ve referenciados
Errores
debug: Depurador y pantalla rojarelease: Pantalla gris o nada, sin informe de errores
Dónde se prueba
debug: Emulador o móvilrelease: Solo en móvil físico

La compilación de release no es la de debug con otro nombre. Es otra app, y merece su propia QA.

Causa 01 / 08

R8 borra algo que se usaba por reflexión

En release, Android pasa tu código por R8, que elimina lo que cree que nadie usa y renombra el resto. Flutter lo activa siempre en las compilaciones de release, y desde el Android Gradle Plugin 8.0 viene en modo completo por defecto, más agresivo. El problema: R8 no ve el código que se invoca por reflexión. Si una librería crea una clase por su nombre, R8 puede haberse cargado justo eso.

Ficha de incidente · Pax

La app se cerraba antes de pintar nada

cierres
85
usuarios
9
semana
1
El error · Crashlytics
Failed to create an instance of androidx.work.impl.WorkDatabase
La cadena
Librería de anuncios
WorkManager, dependencia transitiva
Room, por reflexión
Causa final: R8 borra el constructor de WorkDatabase_Impl
Arreglo · una línea en proguard-rules.pro
-keep class * extends androidx.room.RoomDatabase { <init>(); }

La pista la dio el propio R8: el usage.txt de build/app/outputs/mapping/release/ lista lo que eliminó.

La pista la dio el propio R8. Junto a cada compilación deja en build/app/outputs/mapping/release/ un usage.txt con lo que eliminó y un seeds.txt con lo que conservó por una regla. Ahí aparecía el constructor borrado; tras el arreglo, ya no. La documentación de Android lo resume bien: si ves ClassNotFoundException, NoSuchMethodException o parecidos solo en release, sospecha de reflexión y añade una regla -keep.

Causa 02 / 08

R8 también rompe cosas sin cerrar la app

El cierre al arrancar al menos se nota. Peor es lo que falla en silencio. En Pax, la notificación diaria —el gancho central de la app— nunca se programaba en release. El logcat decía TypeToken must be created with a type argument: R8 había quitado las firmas genéricas que Gson necesita dentro del plugin flutter_local_notifications. Y el mismo fallo tumbaba el receptor que reprograma las notificaciones al reiniciar el móvil.

Arreglo: -keepattributes Signature más las reglas de Gson y del plugin. La lección general: cuando metes una dependencia nueva con código nativo, revisa si documenta reglas de R8, y prueba la función que la usa en release, no solo que la app abra.

Causa 03 / 08

La app publicada lleva otra firma (y nadie la conoce)

Tu app tiene al menos tres certificados: el de depuración que genera el SDK, el de subida con el que firmas el bundle y el de firma de apps de Play, porque con Play App Signing Google vuelve a firmar lo que subes antes de entregarlo a los usuarios. Firebase, el login con Google y cualquier clave de API restringida a «Apps para Android» comprueban esa huella.

En Pax, el login con Google funcionaba en debug y en release devolvía Requests from this Android client application ... are blocked. Dos huecos a la vez: la clave de API de Android solo tenía autorizada la huella de depuración, y Firebase no conocía la de Play App Signing. Resultado: los ocho testers de la prueba cerrada no podían entrar. El arreglo no necesitó recompilar, porque la validación es del lado del servidor: añadir las huellas que faltaban y esperar a que se propagaran.

Causa 04 / 08

Recursos que desaparecen del APK

Con shrinkResources activo, el empaquetado elimina los recursos de Android que no ve referenciados. Si un recurso solo se nombra desde Dart, para Android no existe. Nos pasó con el icono pequeño de la notificación de Pax, que se pasa por nombre desde el código Flutter: lo protegimos con un res/raw/keep.xml y su atributo tools:keep antes de que la compilación de release lo dejara fuera.

Causa 05 / 08

Sin permiso de internet, o con HTTP a pelo

Un clásico de Flutter: la plantilla de proyecto declara android.permission.INTERNET en los manifiestos de debug y profile, porque las herramientas lo necesitan para la recarga en caliente. Si tu manifiesto principal no lo declara, en debug hay red y en release no. Las llamadas fallan y, según cómo trates el error, la app solo muestra listas vacías.

El primo de este fallo: desde Android 9, las apps que apuntan a ese nivel de API o superior tienen el tráfico HTTP sin cifrar bloqueado por defecto. Si en desarrollo apuntabas a un servidor local por http://, en producción todo debe ir por HTTPS, y la excepción, si de verdad la necesitas, se declara en la configuración de seguridad de red solo para ese dominio.

Causa 06 / 08

Código que solo corre en debug sin que lo sepas

En release, Flutter desactiva las aserciones: todo lo que esté dentro de un assert(...) no se ejecuta. Si dentro había una inicialización «de paso», en release no ocurre. Lo mismo con ramas condicionadas a kDebugMode, valores que llegan por --dart-define en tu comando local y no en el de compilación, o un google-services.json distinto por entorno.

  • Busca assert( y kDebugMode en el código y comprueba que ninguno esconde lógica que la app necesita.
  • Escribe en un script el comando exacto de compilación de release, con todos sus --dart-define. Que no dependa de la memoria de nadie.
  • Comprueba que la configuración de Firebase del bundle es la del proyecto de producción.
Causa 07 / 08

Debug miente sobre el rendimiento (en los dos sentidos)

El modo debug está pensado para iterar rápido, no para ir rápido. Una animación que tartamudea en debug puede ir perfecta en release, y al revés: una carrera entre dos tareas asíncronas que en debug nunca se cruzaban porque todo iba lento puede aparecer cuando la app compila optimizada. Para medir rendimiento, Flutter tiene el modo profile; para cazar carreras, prueba en release en un móvil modesto, no solo en el tuyo.

Causa 08 / 08

Los logs desaparecen y la red de la oficina es demasiado buena

En release no hay depurador ni pantalla roja de error: el fallo se queda en una pantalla gris o en nada. Si no tienes un informe de errores, los cierres de tus usuarios no los ves. Y hay fallos que no son de release pero se esconden igual de bien: Pax descargaba sus fuentes de Google Fonts al arrancar, y una conexión que se cortó a mitad provocó un error no capturado que Crashlytics nos trajo de los testers. Lo arreglamos empaquetando las fuentes, desactivando la descarga en tiempo de ejecución y con un test que falla si alguien vuelve a pedir una por red.

El método

Cómo cazarlo antes que tus usuarios

Ninguno de estos fallos se ve en debug, y casi ninguno en los tests unitarios. La única defensa es probar la compilación de release de verdad, antes de cada subida. Flutter no ejecuta release en emuladores, así que hace falta un móvil físico. Nuestra rutina:

  1. Compila en release (flutter build apk --release, o genera APKs desde el AAB con bundletool) e instálala en un móvil real con flutter install o adb install.
  2. Deja adb logcat abierto y recorre lo que da dinero o retiene: arranque en frío, login, compra, notificaciones, permisos. Cinco minutos mínimo.
  3. Reinicia el móvil y abre la app otra vez: los receptores de arranque fallan ahí.
  4. Prueba con el modo avión a mitad de una carga.
  5. Si has metido una dependencia nueva, mira el usage.txt de R8 antes de subir.
  6. Sube primero a la pista de prueba interna o cerrada: es la única forma de probar con la firma de Play, la que de verdad llegará a los usuarios.
La compilación de release no es la de debug con otro nombre. Es otra app, y merece su propia QA.
Contacto

¿Tu app se cae en release y no sabes por qué?

Revisamos la compilación, el R8 y las firmas, y la dejamos lista para Google Play sin sustos.

Hablemos

Sigue leyendo

Todos los artículos →
Publicar apps · 8 min

Publicar tu primera app en Google Play en 2026: los 12 testers, los 14 días y lo que nadie cuenta

Publicar apps · 8 min

Flutter o nativo para la app de tu empresa: lo que aprendimos con Tajo y Pax