15 juin 2026 · Flutter · go_router
go_router : quand state.extra disparaît avec refreshListenable
Un bug subtil en Flutter : les données passées via extra sont perdues quand le routeur se reconstruit. Diagnostic et solution durable.
Sur un flux d'authentification par OTP, j'ai rencontré un bug déroutant : après validation du code, l'utilisateur était renvoyé vers l'écran de connexion au lieu de continuer vers son profil. Aucune erreur, aucun log suspect : juste une redirection fantôme.
Le symptôme
Le numéro de téléphone était transmis à l'écran de vérification OTP via le
paramètre extra de go_router :
context.push('/verify-otp', extra: phoneNumber);
Dans la majorité des cas, tout fonctionnait. Mais dès que l'état
d'authentification changeait pendant que l'écran OTP était affiché, le
state.extra devenait null et la garde de route renvoyait l'utilisateur
au point de départ.
La cause
Le routeur était configuré avec un refreshListenable branché sur le bloc
d'authentification. C'est le pattern classique pour déclencher les
redirections quand l'utilisateur se connecte ou se déconnecte :
GoRouter(
refreshListenable: GoRouterRefreshStream(authBloc.stream),
redirect: (context, state) { /* ... */ },
// ...
)
Or, quand le refreshListenable notifie, go_router réévalue la route
courante en reconstruisant un nouveau state, et extra n'est pas
conservé dans cette reconstruction. extra est un objet Dart passé en
mémoire au moment de la navigation ; il ne fait pas partie de l'URL, donc il
ne survit pas à une réévaluation.
C'est un piège documenté mais facile à manquer : extra est fiable pour une
navigation ponctuelle, pas pour un état qui doit survivre aux rebuilds du
routeur.
La solution : faire porter l'état par l'URL
La correction consiste à passer les données indispensables dans les paramètres de requête, qui font partie de l'URL et survivent donc à toutes les réévaluations :
// Un helper centralisé plutôt que des URLs construites à la main
abstract class Routes {
static String verifyOtpWith(String phone) =>
'/verify-otp?phone=${Uri.encodeComponent(phone)}';
}
// À l'appel
context.push(Routes.verifyOtpWith(phoneNumber));
// À la lecture
final phone = state.uri.queryParameters['phone'];
Deux règles que j'en ai tirées :
extrauniquement pour du confort éphémère (un objet déjà chargé qu'on évite de recharger), jamais pour une donnée dont dépend la logique de navigation.- Centraliser la construction des routes dans une classe dédiée. Le jour où un paramètre change, il n'y a qu'un seul endroit à modifier, et l'encodage URL est garanti partout.
Ce genre de bug coûte des heures parce qu'il est intermittent : il ne se
manifeste que si le refreshListenable notifie au mauvais moment. Si votre
flux OTP ou deep-link « oublie » ses données par moments, vérifiez ce que
vous transportez dans extra.