Flutter: eliminar la pantalla gris de la muerte en producción

Sommaire
Introducción – por qué la pantalla gris es un problema
Cuando el build, el layout o el paint de un widget fallan, Flutter llama a ErrorWidget.builder.
En modo debug, el framework muestra un rectángulo rojo detallando la traza de la pila ; en release, muestra un fondo gris neutro para no exponer información sensible al público (Manejo de errores en Flutter).
Feo y sin texto, ¡esa pantalla no tranquiliza al usuario!
Comprender la cadena de gestión de errores de Flutter
Esta sección aclara quién captura el error, cuándo, y cómo personalizar la respuesta.
1. FlutterError.onError : el primer eslabón
- Rol : manejador global invocado para cualquier error detectado en la pila de Flutter.
- Por defecto : llama a
FlutterError.presentError, que se limita a imprimir el registro. - Personalización : reemplácelo por su función ; aún puede llamar a
presentErrorpara mantener la traza en la consola (propiedad onError - clase FlutterError - biblioteca foundation - API de Dart). - Alcance : errores síncronos del framework, incluyendo aquellos desencadenados durante
setState,build,layoutopaint.
🧠 Buen dato – Como se ejecuta antes del renderizado,
onErrorle permite enrutar la excepción a Crashlytics mientras deja el renderizado aErrorWidget.builder.
2. ErrorWidget.builder : la pantalla que reemplaza al widget defectuoso
- ¿Cuándo se llama ? Cuando ocurre una excepción durante la fase build.
- Comportamiento por defecto :
- Debug/Profile : rectángulo rojo + mensaje.
- Release : fondo gris uniforme (también conocido como grey screen of death) (Manejo de errores en Flutter).
- Personalización : defina su propio builder para devolver un componente amigable para UX — texto claro, suficiente contraste, botón de regreso.
3. Zonas Dart y runZonedGuarded : capturar lo asíncrono
- Problema : los futures fuera del ciclo de Flutter no llegan a
onError. - Solución : rodee
runAppcon unrunZonedGuarded; cualquier error asíncrono en esa zona se redirige a su callbackonErrorpersonalizado (función runZonedGuarded - biblioteca dart:async - API de Dart - Flutter, Zonas | Dart). - Bonus : en producción, esto evita un crash del Isolate o el bloqueo completo de la aplicación.
4. PlatformDispatcher.onError : último recurso nativo
- Contexto : un error puede provenir de un plugin o de un canal de método ejecutado del lado del motor (C++/JNI).
- Callback :
PlatformDispatcher.instance.onErrorrecibe la excepción y la pila nativa. - Convención : devuelva
truesi ha manejado el problema ; de lo contrario Flutter terminará el proceso (Manejo de errores en Flutter).
➡️ Secuencia temporal
| Paso | ¿Quién captura ? | Ejemplo de error | Posibilidades de personalización |
|---|---|---|---|
| 1 | FlutterError.onError | setState en widget desmontado | Registro + notificar Crashlytics |
| 2 | ErrorWidget.builder | División por cero en build() | Renderizado de una pantalla de emergencia |
| 3 | runZonedGuarded | Tiempo de espera en un Future | Reinicio automático de la zona |
| 4 | PlatformDispatcher.onError | NullPointerException en Android | Mostrar una pantalla nativa de respaldo |
Así, ninguna excepción debería llegar al usuario sin pasar por al menos uno de estos ganchos.
Tutorial paso a paso
1. Inicializar un Error Boundary global (main.dart)
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Capture Flutter synchrones
FlutterError.onError = (details) {
FlutterError.presentError(details); // Log locale
_report(details.exception, details.stack); // Crashlytics / Sentry
};
// 2. Capture erreurs natives (plugins, channels)
PlatformDispatcher.instance.onError = (error, stack) {
_report(error, stack);
return true; // Erreur considérée gérée
};
// 3. Capture erreurs asynchrones hors pile Flutter
runZonedGuarded(
() => runApp(const MyApp()),
(error, stack) => _report(error, stack),
);
}
void _report(Object error, StackTrace? stack) {
// TODO : intégrer FirebaseCrashlytics.instance.recordError(...)
}
Cette triple interception couvre les cas documentés par l'équipe Flutter (Errores comunes de Flutter, Manejo de errores en Flutter).
2. Crear un CustomErrorScreen accesible (WCAG 2.2)
class CustomErrorScreen extends StatelessWidget {
final Object error;
const CustomErrorScreen(this.error, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Semantics(
label: 'Erreur critique',
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 96),
const SizedBox(height: 24),
Text(
'Oups ! Un incident est survenu.',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
'Notre équipe a été notifiée. Vous pouvez relancer l'application.',
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () => runApp(const MyApp()),
child: const Text('Redémarrer'),
),
],
),
),
),
),
);
}
}
- Contraste mínimo 4,5:1 (WCAG 2.2 SC 1.4.3).
- Mensaje descriptivo : cumple el criterio 3.3.1 "Identificación de errores" (Comprender el criterio de éxito 3.3.1: Identificación de errores | WAI - W3C).
- Activación :
ErrorWidget.builder = (details) => CustomErrorScreen(details.exception);
3. Registrar en Crashlytics o Sentry
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
void _report(Object error, StackTrace? stack) {
FirebaseCrashlytics.instance.recordError(error, stack);
// Ou: Sentry.captureException(error, stackTrace: stack);
}
Estas plataformas agrupan el error, la pila, el contexto del dispositivo y disparan alertas en tiempo real.
4. Pruebas unitarias y pruebas de widgets
testWidgets('CustomErrorScreen remplace le widget fautif', (tester) async {
ErrorWidget.builder = (d) => CustomErrorScreen(d.exception);
await tester.pumpWidget(const BrokenWidget()); // Simule un widget erroné
expect(find.byType(CustomErrorScreen), findsOneWidget);
});
La guía Manejo de errores en Flutter detalla la inyección de un MaterialApp.builder para pruebas más complejas (Manejo de errores en Flutter).
Conclusión
Puntos clave
- La pantalla gris oculta una excepción no gestionada ; perjudica la confianza y la retención.
- Combine
FlutterError.onError→ErrorWidget.builder→runZonedGuarded→PlatformDispatcher.onErrorpara capturar todos los escenarios. - Muestre un
CustomErrorScreenconforme a WCAG, luego registre el incidente en Crashlytics o Sentry.
Comentarios
Cargando...