Flutter : supprimer l’écran gris de la mort en production

Sommaire
Introduction – pourquoi l’écran gris est un problème
Lorsque le build, le layout ou le paint d’un widget échouent, Flutter appelle ErrorWidget.builder
.
En debug, le framework montre un rectangle rouge détaillant la stack trace ; en release, il rend un fond gris neutre pour ne pas exposer d’informations sensibles au public (Handling errors in Flutter).
Disgracieux et sans texte, cet écran ne rassure pas l’utilisateur!
Comprendre la chaîne de gestion des erreurs Flutter
Cette section clarifie qui capte l’erreur, quand, et comment personnaliser la réponse.
1. FlutterError.onError
: le premier maillon
- Rôle : handler global invoqué pour toute erreur détectée sur la pile Flutter.
- Défaut : appelle
FlutterError.presentError
, qui se contente d’imprimer la log. - Personnalisation : remplacez-le par votre fonction ; vous pouvez toujours appeler
presentError
pour garder la trace console (onError property - FlutterError class - foundation library - Dart API). - Portée : erreurs synchrones du framework, y compris celles déclenchées durant
setState
,build
,layout
oupaint
.
🧠 Bon à savoir – Parce qu’il s’exécute avant le rendu,
onError
vous permet de router l’exception vers Crashlytics tout en laissant le rendu àErrorWidget.builder
.
2. ErrorWidget.builder
: l’écran qui remplace le widget fautif
- Quand est-il appelé ? Lorsqu’une exception survient pendant la phase build.
- Comportement par défaut :
- Debug/Profile : rectangle rouge + message.
- Release : fond gris uni (alias grey screen of death) (Handling errors in Flutter).
- Personnalisation : définissez votre propre builder pour retourner un composant UX-friendly — texte clair, contraste suffisant, bouton retour.
3. Zones Dart et runZonedGuarded
: capter l’asynchrone
- Problème : les futures détachés du cycle Flutter n’arrivent pas à
onError
. - Solution : entourez
runApp
d’unrunZonedGuarded
; toute erreur asynchrone dans cette zone est redirigée vers votre callbackonError
personnalisé (runZonedGuarded function - dart:async library - Dart API - Flutter, Zones | Dart). - Bonus : en production, cela évite un crash d’Isolate ou le blocage complet de l’application.
4. PlatformDispatcher.onError
: dernier rempart natif
- Contexte : une erreur peut provenir d’un plugin ou d’un canal de méthode exécuté côté moteur (C++/JNI).
- Callback :
PlatformDispatcher.instance.onError
reçoit l’exception et la stack native. - Convention : retournez
true
si vous avez géré le problème ; sinon Flutter interrompra le processus (Handling errors in Flutter).
➡️ Enchaînement temporel
Étape | Qui capture ? | Exemple d’erreur | Possibilités de customisation |
---|---|---|---|
1 | FlutterError.onError | setState sur widget démonté | Log + trigger Crashlytics |
2 | ErrorWidget.builder | Division by zero dans build() | Rendu d’un écran de secours |
3 | runZonedGuarded | Timeout dans un Future | Relance automatique de la zone |
4 | PlatformDispatcher.onError | NullPointerException Android | Affichage d’une native fallback screen |
Ainsi, aucune exception ne devrait atteindre l’utilisateur sans passer par au moins un de ces crochets.
Tutoriel pas-à-pas
1. Initialiser 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 (Common Flutter errors, Handling errors in Flutter).
2. Créer un CustomErrorScreen
accessible (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 minimum 4,5:1 (WCAG 2.2 SC 1.4.3).
- Message descriptif : satisfait le critère 3.3.1 “Error Identification” (Understanding Success Criterion 3.3.1: Error Identification | WAI - W3C).
- Activation :
ErrorWidget.builder = (details) => CustomErrorScreen(details.exception);
3. Journaliser vers Crashlytics ou 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);
}
Ces plateformes regroupent l’erreur, la stack, le contexte appareil et déclenchent des alertes en temps réel.
4. Tests unitaires & widget tests
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);
});
Le guide Handling errors in Flutter détaille l’injection d’un MaterialApp.builder
pour des tests plus complexes (Handling errors in Flutter).
Conclusion
Points clés
- Le grey screen masque une exception non gérée ; il nuit à la confiance et à la rétention.
- Combinez
FlutterError.onError
→ErrorWidget.builder
→runZonedGuarded
→PlatformDispatcher.onError
pour capturer tous les scénarios. - Affichez un
CustomErrorScreen
conforme WCAG, puis tracez l’incident vers Crashlytics ou Sentry.