[{"data":1,"prerenderedAt":376},["ShallowReactive",2],{"article-\u002Fblog\u002Fgo-router-state-extra-refreshlistenable":3},{"id":4,"title":5,"body":6,"brouillon":363,"date":364,"description":365,"extension":366,"image":367,"imageAlt":367,"meta":368,"navigation":265,"path":369,"seo":370,"stem":371,"tags":372,"__hash__":375},"blog\u002Fblog\u002Fgo-router-state-extra-refreshlistenable.md","go_router : quand state.extra disparaît avec refreshListenable",{"type":7,"value":8,"toc":358},"minimark",[9,13,18,26,65,76,80,87,145,165,171,175,178,325,328,346,354],[10,11,12],"p",{},"Sur un flux d'authentification par OTP, j'ai rencontré un bug déroutant :\naprès validation du code, l'utilisateur était renvoyé vers l'écran de\nconnexion au lieu de continuer vers son profil. Aucune erreur, aucun log\nsuspect : juste une redirection fantôme.",[14,15,17],"h2",{"id":16},"le-symptôme","Le symptôme",[10,19,20,21,25],{},"Le numéro de téléphone était transmis à l'écran de vérification OTP via le\nparamètre ",[22,23,24],"code",{},"extra"," de go_router :",[27,28,33],"pre",{"className":29,"code":30,"language":31,"meta":32,"style":32},"language-dart shiki shiki-themes github-dark","context.push('\u002Fverify-otp', extra: phoneNumber);\n","dart","",[22,34,35],{"__ignoreMap":32},[36,37,40,44,48,51,55,58,62],"span",{"class":38,"line":39},"line",1,[36,41,43],{"class":42},"s95oV","context.",[36,45,47],{"class":46},"svObZ","push",[36,49,50],{"class":42},"(",[36,52,54],{"class":53},"sU2Wk","'\u002Fverify-otp'",[36,56,57],{"class":42},", extra",[36,59,61],{"class":60},"snl16",":",[36,63,64],{"class":42}," phoneNumber);\n",[10,66,67,68,71,72,75],{},"Dans la majorité des cas, tout fonctionnait. Mais dès que l'état\nd'authentification changeait pendant que l'écran OTP était affiché, le\n",[22,69,70],{},"state.extra"," devenait ",[22,73,74],{},"null"," et la garde de route renvoyait l'utilisateur\nau point de départ.",[14,77,79],{"id":78},"la-cause","La cause",[10,81,82,83,86],{},"Le routeur était configuré avec un ",[22,84,85],{},"refreshListenable"," branché sur le bloc\nd'authentification. C'est le pattern classique pour déclencher les\nredirections quand l'utilisateur se connecte ou se déconnecte :",[27,88,90],{"className":29,"code":89,"language":31,"meta":32,"style":32},"GoRouter(\n  refreshListenable: GoRouterRefreshStream(authBloc.stream),\n  redirect: (context, state) { \u002F* ... *\u002F },\n  \u002F\u002F ...\n)\n",[22,91,92,101,115,133,139],{"__ignoreMap":32},[36,93,94,98],{"class":38,"line":39},[36,95,97],{"class":96},"sDLfK","GoRouter",[36,99,100],{"class":42},"(\n",[36,102,104,107,109,112],{"class":38,"line":103},2,[36,105,106],{"class":42},"  refreshListenable",[36,108,61],{"class":60},[36,110,111],{"class":96}," GoRouterRefreshStream",[36,113,114],{"class":42},"(authBloc.stream),\n",[36,116,118,121,123,126,130],{"class":38,"line":117},3,[36,119,120],{"class":42},"  redirect",[36,122,61],{"class":60},[36,124,125],{"class":42}," (context, state) { ",[36,127,129],{"class":128},"sAwPA","\u002F* ... *\u002F",[36,131,132],{"class":42}," },\n",[36,134,136],{"class":38,"line":135},4,[36,137,138],{"class":128},"  \u002F\u002F ...\n",[36,140,142],{"class":38,"line":141},5,[36,143,144],{"class":42},")\n",[10,146,147,148,150,151,158,159,161,162,164],{},"Or, quand le ",[22,149,85],{}," notifie, go_router ",[152,153,154,155],"strong",{},"réévalue la route\ncourante en reconstruisant un nouveau ",[22,156,157],{},"state",", et ",[22,160,24],{}," n'est pas\nconservé dans cette reconstruction. ",[22,163,24],{}," est un objet Dart passé en\nmémoire au moment de la navigation ; il ne fait pas partie de l'URL, donc il\nne survit pas à une réévaluation.",[10,166,167,168,170],{},"C'est un piège documenté mais facile à manquer : ",[22,169,24],{}," est fiable pour une\nnavigation ponctuelle, pas pour un état qui doit survivre aux rebuilds du\nrouteur.",[14,172,174],{"id":173},"la-solution-faire-porter-létat-par-lurl","La solution : faire porter l'état par l'URL",[10,176,177],{},"La correction consiste à passer les données indispensables dans les\nparamètres de requête, qui font partie de l'URL et survivent donc à toutes\nles réévaluations :",[27,179,181],{"className":29,"code":180,"language":31,"meta":32,"style":32},"\u002F\u002F Un helper centralisé plutôt que des URLs construites à la main\nabstract class Routes {\n  static String verifyOtpWith(String phone) =>\n      '\u002Fverify-otp?phone=${Uri.encodeComponent(phone)}';\n}\n\n\u002F\u002F À l'appel\ncontext.push(Routes.verifyOtpWith(phoneNumber));\n\n\u002F\u002F À la lecture\nfinal phone = state.uri.queryParameters['phone'];\n",[22,182,183,188,202,224,255,260,267,273,293,298,304],{"__ignoreMap":32},[36,184,185],{"class":38,"line":39},[36,186,187],{"class":128},"\u002F\u002F Un helper centralisé plutôt que des URLs construites à la main\n",[36,189,190,193,196,199],{"class":38,"line":103},[36,191,192],{"class":60},"abstract",[36,194,195],{"class":60}," class",[36,197,198],{"class":96}," Routes",[36,200,201],{"class":42}," {\n",[36,203,204,207,210,213,215,218,221],{"class":38,"line":117},[36,205,206],{"class":60},"  static",[36,208,209],{"class":96}," String",[36,211,212],{"class":46}," verifyOtpWith",[36,214,50],{"class":42},[36,216,217],{"class":96},"String",[36,219,220],{"class":42}," phone) ",[36,222,223],{"class":60},"=>\n",[36,225,226,229,232,235,238,241,243,246,249,252],{"class":38,"line":135},[36,227,228],{"class":53},"      '\u002Fverify-otp?phone=",[36,230,231],{"class":53},"${",[36,233,234],{"class":96},"Uri",[36,236,237],{"class":53},".",[36,239,240],{"class":46},"encodeComponent",[36,242,50],{"class":53},[36,244,245],{"class":96},"phone",[36,247,248],{"class":53},")}",[36,250,251],{"class":53},"'",[36,253,254],{"class":42},";\n",[36,256,257],{"class":38,"line":141},[36,258,259],{"class":42},"}\n",[36,261,263],{"class":38,"line":262},6,[36,264,266],{"emptyLinePlaceholder":265},true,"\n",[36,268,270],{"class":38,"line":269},7,[36,271,272],{"class":128},"\u002F\u002F À l'appel\n",[36,274,276,278,280,282,285,287,290],{"class":38,"line":275},8,[36,277,43],{"class":42},[36,279,47],{"class":46},[36,281,50],{"class":42},[36,283,284],{"class":96},"Routes",[36,286,237],{"class":42},[36,288,289],{"class":46},"verifyOtpWith",[36,291,292],{"class":42},"(phoneNumber));\n",[36,294,296],{"class":38,"line":295},9,[36,297,266],{"emptyLinePlaceholder":265},[36,299,301],{"class":38,"line":300},10,[36,302,303],{"class":128},"\u002F\u002F À la lecture\n",[36,305,307,310,313,316,319,322],{"class":38,"line":306},11,[36,308,309],{"class":60},"final",[36,311,312],{"class":42}," phone ",[36,314,315],{"class":60},"=",[36,317,318],{"class":42}," state.uri.queryParameters[",[36,320,321],{"class":53},"'phone'",[36,323,324],{"class":42},"];\n",[10,326,327],{},"Deux règles que j'en ai tirées :",[329,330,331,340],"ul",{},[332,333,334,339],"li",{},[152,335,336,338],{},[22,337,24],{}," uniquement pour du confort éphémère"," (un objet déjà chargé\nqu'on évite de recharger), jamais pour une donnée dont dépend la logique\nde navigation.",[332,341,342,345],{},[152,343,344],{},"Centraliser la construction des routes"," dans une classe dédiée. Le jour\noù un paramètre change, il n'y a qu'un seul endroit à modifier, et\nl'encodage URL est garanti partout.",[10,347,348,349,351,352,237],{},"Ce genre de bug coûte des heures parce qu'il est intermittent : il ne se\nmanifeste que si le ",[22,350,85],{}," notifie au mauvais moment. Si votre\nflux OTP ou deep-link « oublie » ses données par moments, vérifiez ce que\nvous transportez dans ",[22,353,24],{},[355,356,357],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":32,"searchDepth":103,"depth":103,"links":359},[360,361,362],{"id":16,"depth":103,"text":17},{"id":78,"depth":103,"text":79},{"id":173,"depth":103,"text":174},false,"2026-06-15","Un bug subtil en Flutter : les données passées via extra sont perdues quand le routeur se reconstruit. Diagnostic et solution durable.","md",null,{},"\u002Fblog\u002Fgo-router-state-extra-refreshlistenable",{"title":5,"description":365},"blog\u002Fgo-router-state-extra-refreshlistenable",[373,374],"Flutter","go_router","Jd7B4YC9yg_fyJWx3u2ubjFgAwz9BnUm2yn0S_4XE5I",1784652016702]