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

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

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 ou paint.

🧠 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 :
  • 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’un runZonedGuarded ; toute erreur asynchrone dans cette zone est redirigée vers votre callback onError 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

ÉtapeQui capture ?Exemple d’erreurPossibilités de customisation
1FlutterError.onErrorsetState sur widget démontéLog + trigger Crashlytics
2ErrorWidget.builderDivision by zero dans build()Rendu d’un écran de secours
3runZonedGuardedTimeout dans un FutureRelance automatique de la zone
4PlatformDispatcher.onErrorNullPointerException AndroidAffichage 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'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
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.onErrorErrorWidget.builderrunZonedGuardedPlatformDispatcher.onError pour capturer tous les scénarios.
  • Affichez un CustomErrorScreen conforme WCAG, puis tracez l’incident vers Crashlytics ou Sentry.

Tags

  • flutter
  • ios
  • mobile
  • dart

Cet article à été posté le