[{"data":1,"prerenderedAt":2457},["ShallowReactive",2],{"navigation_docs_fr":3,"-fr-examples-infinite-scroll":342,"-fr-examples-infinite-scroll-surround":2452},[4,45,91,137,195,236,257,291,327],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Prise en main","i-lucide-rocket","/fr/getting-started","fr/01.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Qu'est-ce que Qora ?","/fr/getting-started/what-is-qora","fr/01.getting-started/2.what-is-qora","i-lucide-info",{"title":16,"path":17,"stem":18,"icon":19},"Installation","/fr/getting-started/installation","fr/01.getting-started/3.installation","i-lucide-download",{"title":21,"path":22,"stem":23,"icon":6},"Démarrage rapide","/fr/getting-started/quick-start","fr/01.getting-started/4.quick-start",{"title":25,"path":26,"stem":27,"icon":28},"Comparaison","/fr/getting-started/comparison","fr/01.getting-started/5.comparison","i-lucide-scale",{"title":30,"path":31,"stem":32,"icon":33},"Venant de TanStack Query","/fr/getting-started/coming-from-tanstack-query","fr/01.getting-started/6.coming-from-tanstack-query","i-lucide-arrow-left-right",{"title":35,"path":36,"stem":37,"icon":38},"Migrer depuis d'Autres Librairies","/fr/getting-started/migrating-from-other-libraries","fr/01.getting-started/7.migrating-from-other-libraries","i-lucide-arrow-right-left",{"title":40,"path":41,"stem":42,"icon":43},"Assistants IA","/fr/getting-started/ai-assistants","fr/01.getting-started/8.ai-assistants","i-lucide-sparkles",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Concepts clés","i-lucide-book-open","/fr/core-concepts","fr/02.core-concepts",[51,56,61,66,71,76,81,86],{"title":52,"path":53,"stem":54,"icon":55},"Requêtes","/fr/core-concepts/queries","fr/02.core-concepts/1.queries","i-lucide-database",{"title":57,"path":58,"stem":59,"icon":60},"Clés de requête","/fr/core-concepts/query-keys","fr/02.core-concepts/2.query-keys","i-lucide-key-round",{"title":62,"path":63,"stem":64,"icon":65},"États des requêtes","/fr/core-concepts/query-states","fr/02.core-concepts/3.query-states","i-lucide-activity",{"title":67,"path":68,"stem":69,"icon":70},"Cache","/fr/core-concepts/caching","fr/02.core-concepts/4.caching","i-lucide-layers-3",{"title":72,"path":73,"stem":74,"icon":75},"Stale-While-Revalidate","/fr/core-concepts/stale-while-revalidate","fr/02.core-concepts/5.stale-while-revalidate","i-lucide-timer-reset",{"title":77,"path":78,"stem":79,"icon":80},"Déduplication","/fr/core-concepts/deduplication","fr/02.core-concepts/6.deduplication","i-lucide-copy-minus",{"title":82,"path":83,"stem":84,"icon":85},"Mutations et invalidation","/fr/core-concepts/mutations","fr/02.core-concepts/7.mutations","i-lucide-pencil-line",{"title":87,"path":88,"stem":89,"icon":90},"Hors-ligne et réseau","/fr/core-concepts/offline","fr/02.core-concepts/8.offline","i-lucide-wifi-off",{"title":92,"icon":93,"path":94,"stem":95,"children":96,"page":44},"Usage avec Flutter","i-lucide-smartphone","/fr/flutter-integration","fr/03.flutter-integration",[97,102,107,112,117,122,127,132],{"title":98,"path":99,"stem":100,"icon":101},"Configuration","/fr/flutter-integration/setup","fr/03.flutter-integration/1.setup","i-lucide-wrench",{"title":103,"path":104,"stem":105,"icon":106},"QoraScope","/fr/flutter-integration/qora-scope","fr/03.flutter-integration/2.qora-scope","i-lucide-share-2",{"title":108,"path":109,"stem":110,"icon":111},"QoraBuilder","/fr/flutter-integration/qora-builder","fr/03.flutter-integration/3.qora-builder","i-lucide-blocks",{"title":113,"path":114,"stem":115,"icon":116},"QoraMutationBuilder","/fr/flutter-integration/qora-mutation-builder","fr/03.flutter-integration/4.qora-mutation-builder","i-lucide-pencil",{"title":118,"path":119,"stem":120,"icon":121},"InfiniteQueryBuilder","/fr/flutter-integration/infinite-query-builder","fr/03.flutter-integration/5.infinite-query-builder","i-lucide-infinity",{"title":123,"path":124,"stem":125,"icon":126},"Hooks (flutter_hooks)","/fr/flutter-integration/hooks","fr/03.flutter-integration/6.hooks","i-lucide-anchor",{"title":128,"path":129,"stem":130,"icon":131},"Widgets de statut réseau","/fr/flutter-integration/network-status","fr/03.flutter-integration/7.network-status","i-lucide-wifi",{"title":133,"path":134,"stem":135,"icon":136},"Bonnes pratiques","/fr/flutter-integration/best-practices","fr/03.flutter-integration/8.best-practices","i-lucide-badge-check",{"title":138,"icon":139,"path":140,"stem":141,"children":142,"page":44},"Guides Pratiques","i-lucide-chef-hat","/fr/recipes","fr/04.recipes",[143,147,152,157,162,166,171,176,181,186,191],{"title":144,"path":145,"stem":146,"icon":85},"Mutations","/fr/recipes/mutations","fr/04.recipes/02.mutations",{"title":148,"path":149,"stem":150,"icon":151},"Mises à jour optimistes","/fr/recipes/optimistic-updates","fr/04.recipes/03.optimistic-updates","i-lucide-zap",{"title":153,"path":154,"stem":155,"icon":156},"Persistance","/fr/recipes/persistence","fr/04.recipes/04.persistence","i-lucide-hard-drive",{"title":158,"path":159,"stem":160,"icon":161},"Réseau adaptatif","/fr/recipes/network-aware","fr/04.recipes/05.network-aware","i-lucide-signal",{"title":163,"path":164,"stem":165,"icon":121},"Requêtes infinies","/fr/recipes/infinite-queries","fr/04.recipes/06.infinite-queries",{"title":167,"path":168,"stem":169,"icon":170},"Requêtes dépendantes","/fr/recipes/dependent-queries","fr/04.recipes/07.dependent-queries","i-lucide-link-2",{"title":172,"path":173,"stem":174,"icon":175},"Annulation","/fr/recipes/cancel-token","fr/04.recipes/08.cancel-token","i-lucide-circle-slash",{"title":177,"path":178,"stem":179,"icon":180},"Hydratation SSR","/fr/recipes/ssr-hydration","fr/04.recipes/09.ssr-hydration","i-lucide-server",{"title":182,"path":183,"stem":184,"icon":185},"Gestion des erreurs","/fr/recipes/error-handling","fr/04.recipes/10.error-handling","i-lucide-shield-alert",{"title":187,"path":188,"stem":189,"icon":190},"Tests","/fr/recipes/testing","fr/04.recipes/11.testing","i-lucide-flask-conical",{"title":192,"path":193,"stem":194},"Prefetch au Niveau des Routes","/fr/recipes/prefetch","fr/04.recipes/12.prefetch",{"title":196,"icon":197,"path":198,"stem":199,"children":200,"page":44},"Référence de l'API","i-lucide-file-code","/fr/api-reference","fr/05.api-reference",[201,206,210,214,219,224,228,232],{"title":202,"path":203,"stem":204,"icon":205},"QoraClient","/fr/api-reference/qora-client","fr/05.api-reference/1.qora-client","i-lucide-cpu",{"title":207,"path":208,"stem":209,"icon":60},"QoraKey","/fr/api-reference/qora-key","fr/05.api-reference/2.qora-key",{"title":211,"path":212,"stem":213,"icon":65},"QoraState","/fr/api-reference/qora-state","fr/05.api-reference/3.qora-state",{"title":215,"path":216,"stem":217,"icon":218},"QoraOptions","/fr/api-reference/qora-options","fr/05.api-reference/4.qora-options","i-lucide-sliders-horizontal",{"title":220,"path":221,"stem":222,"icon":223},"QoraClientConfig","/fr/api-reference/qora-client-config","fr/05.api-reference/5.qora-client-config","i-lucide-settings-2",{"title":225,"path":226,"stem":227,"icon":85},"API Mutation","/fr/api-reference/qora-mutation","fr/05.api-reference/6.qora-mutation",{"title":229,"path":230,"stem":231,"icon":156},"PersistQoraClient","/fr/api-reference/persist-qora-client","fr/05.api-reference/7.persist-qora-client",{"title":233,"path":234,"stem":235,"icon":121},"Requête infinie","/fr/api-reference/infinite-query","fr/05.api-reference/8.infinite-query",{"title":237,"icon":238,"path":239,"stem":240,"children":241,"page":44},"Outils de debug","i-lucide-bug","/fr/devtools","fr/06.devtools",[242,247,252],{"title":243,"path":244,"stem":245,"icon":246},"Vue d'ensemble","/fr/devtools/overview","fr/06.devtools/1.overview","i-lucide-layout-dashboard",{"title":248,"path":249,"stem":250,"icon":251},"Overlay intégré","/fr/devtools/overlay","fr/06.devtools/2.overlay","i-lucide-layers",{"title":253,"path":254,"stem":255,"icon":256},"Sérialisation des données","/fr/devtools/data-display","fr/06.devtools/4.data-display","i-lucide-eye",{"title":258,"icon":259,"path":260,"stem":261,"children":262,"page":44},"Intégrations tierces","i-lucide-puzzle","/fr/third-party","fr/07.third-party",[263,268,272,277,282,286],{"title":264,"path":265,"stem":266,"icon":267},"Avec Provider","/fr/third-party/with-provider","fr/07.third-party/1.with-provider","i-lucide-plug",{"title":269,"path":270,"stem":271,"icon":111},"Avec BLoC","/fr/third-party/with-bloc","fr/07.third-party/2.with-bloc",{"title":273,"path":274,"stem":275,"icon":276},"Avec Riverpod","/fr/third-party/with-riverpod","fr/07.third-party/3.with-riverpod","i-lucide-droplets",{"title":278,"path":279,"stem":280,"icon":281},"Avec RxDart","/fr/third-party/with-rxdart","fr/07.third-party/4.with-rxdart","i-lucide-git-branch",{"title":283,"path":284,"stem":285},"Avec Dio","/fr/third-party/with-dio","fr/07.third-party/5.with-dio",{"title":287,"path":288,"stem":289,"icon":290},"Avec GraphQL","/fr/third-party/with-graphql","fr/07.third-party/6.with-graphql","i-lucide-binary",{"title":292,"icon":190,"path":293,"stem":294,"children":295,"page":44},"Exemples","/fr/examples","fr/08.examples",[296,301,305,310,314,318,322],{"title":297,"path":298,"stem":299,"icon":300},"Étude de cas : Liste et détail utilisateur","/fr/examples/basic-query","fr/08.examples/1.basic-query","i-lucide-play-circle",{"title":302,"path":303,"stem":304,"icon":151},"Étude de cas : Mutations optimistes et rollback","/fr/examples/mutations-optimistic","fr/08.examples/2.mutations-optimistic",{"title":306,"path":307,"stem":308,"icon":309},"Étude de cas : Feed à défilement infini","/fr/examples/infinite-scroll","fr/08.examples/3.infinite-scroll","i-lucide-scroll",{"title":311,"path":312,"stem":313,"icon":90},"Étude de cas : Application Todo offline-first","/fr/examples/offline-first","fr/08.examples/4.offline-first",{"title":315,"path":316,"stem":317,"icon":161},"Étude de cas : Conscience réseau","/fr/examples/network-awareness","fr/08.examples/5.network-awareness",{"title":319,"path":320,"stem":321,"icon":126},"Étude de cas : Intégration des hooks","/fr/examples/hooks-integration","fr/08.examples/6.hooks-integration",{"title":323,"path":324,"stem":325,"icon":326},"Étude de cas : Application Todo complète","/fr/examples/complete-todo-app","fr/08.examples/7.complete-todo-app","i-lucide-check-square",{"title":328,"icon":329,"path":330,"stem":331,"children":332,"page":44},"Aide","i-lucide-life-buoy","/fr/help","fr/09.help",[333,338],{"title":334,"path":335,"stem":336,"icon":337},"FAQ","/fr/help/faq","fr/09.help/1.faq","i-lucide-circle-help",{"title":339,"path":340,"stem":341,"icon":101},"Dépannage","/fr/help/troubleshooting","fr/09.help/2.troubleshooting",{"id":343,"title":306,"body":344,"description":2443,"extension":2444,"links":2445,"meta":2446,"navigation":2447,"path":307,"seo":2448,"stem":308,"__hash__":2451},"docs_fr/fr/08.examples/3.infinite-scroll.md",{"type":345,"value":346,"toc":2426},"minimark",[347,352,365,372,375,379,465,467,471,489,499,512,825,834,836,840,845,877,890,1125,1131,1137,1715,1740,1746,1758,2016,2022,2035,2264,2266,2270,2276,2285,2287,2291,2373,2375,2379,2422],[348,349,351],"h2",{"id":350},"objectif","Objectif",[353,354,355,359,360,364],"p",{},[356,357,358],"strong",{},"Contexte"," : Un feed à défilement infini semble simple : charger plus de données au fil du scroll. Les contraintes de production sont strictes : après 30 minutes de scroll, votre ",[361,362,363],"code",{},"ListView"," contient 2 000 widgets, la mémoire explose et l'OS termine votre app. Par ailleurs, si vous utilisez des numéros de page entiers et qu'un post est inséré en cours de scroll, chaque page suivante est décalée d'un et les utilisateurs voient des doublons.",[353,366,367,368,371],{},"Cet exemple démontre deux contrôles fondamentaux. La pagination par curseur élimine la dérive des décalages. La pagination fenêtrée avec ",[361,369,370],{},"maxPages"," impose une empreinte mémoire bornée tout en permettant la récupération de pages sur demande.",[373,374],"hr",{},[348,376,378],{"id":377},"fonctionnalités-démontrées","Fonctionnalités démontrées",[380,381,382,398,404,412,424,435,445,453],"ul",{},[383,384,385,389,390,393,394,397],"li",{},[356,386,387],{},[361,388,118],{}," : accumule les pages et expose ",[361,391,392],{},"fetchNextPage"," / ",[361,395,396],{},"fetchPreviousPage"," via son controller",[383,399,400,403],{},[356,401,402],{},"Pagination par curseur"," : résultats stables indépendamment des insertions côté serveur",[383,405,406,411],{},[356,407,408,409],{},"Gestion mémoire fenêtrée avec ",[361,410,370],{}," : plafonne la fenêtre de pages en mémoire ; supprime les plus anciennes/récentes quand dépassé",[383,413,414,423],{},[356,415,416,419,420],{},[361,417,418],{},"isFetchingNextPage"," + ",[361,421,422],{},"isFetchingPreviousPage"," : pilote le loader de fin de scroll sans masquer le contenu existant",[383,425,426,431,432],{},[356,427,428],{},[361,429,430],{},"InfiniteData.flatten()"," : convertit la matrice de pages en liste plate pour ",[361,433,434],{},"SliverList",[383,436,437,440,441,444],{},[356,438,439],{},"Création de post optimiste"," : ",[361,442,443],{},"setInfiniteQueryData"," prépose un nouveau post instantanément ; rollback en cas d'échec",[383,446,447,452],{},[356,448,449],{},[361,450,451],{},"refetch()"," : pull-to-refresh qui refetche toutes les pages chargées en préservant le nombre de pages",[383,454,455,464],{},[356,456,457,460,461],{},[361,458,459],{},"InfiniteFailure"," avec ",[361,462,463],{},"previousData"," : les erreurs de chargement supplémentaire affichent un bouton de réessai sans effacer le feed",[373,466],{},[348,468,470],{"id":469},"points-darchitecture","Points d'architecture",[353,472,473,474,477,478,481,482,485,486,488],{},"La couche UI est construite sur un ",[361,475,476],{},"CustomScrollView"," avec deux slivers : un ",[361,479,480],{},"CupertinoSliverRefreshControl"," pour le pull-to-refresh et un ",[361,483,484],{},"SliverList.builder"," pour les éléments du feed. ",[361,487,118],{}," encapsule la vue de scroll et possède le cycle de vie de l'observer.",[490,491,497],"pre",{"className":492,"code":494,"language":495,"meta":496},[493],"language-text","FeedScreen\n└── CustomScrollView\n    ├── CupertinoSliverRefreshControl   ← controller.refetch() au pull\n    └── SliverList.builder\n        ├── items = data.flatten((p) => p.posts)   ← liste plate de toutes les pages\n        ├── [dernier élément]\n        │   ├── hasNextPage → LoadMoreTrigger       ← déclenche controller.fetchNextPage()\n        │   └── isFetchingNextPage → PageLoader     ← spinner en bas de liste\n        └── InfiniteFailure avec previousData → ErrorRetryBanner\n","text","",[361,498,494],{"__ignoreMap":496},[353,500,501,503,504,507,508,511],{},[361,502,118],{}," est configuré avec un curseur ",[361,505,506],{},"String",". Une chaîne vide signifie « commencer depuis le début » : le serveur renvoie les posts les plus récents et un ",[361,509,510],{},"nextCursor"," pointant vers le contenu plus ancien.",[490,513,518],{"className":514,"code":515,"filename":516,"language":517,"meta":496,"style":496},"language-dart shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","InfiniteQueryBuilder\u003CFeedPage, String>(\n  queryKey: const ['feed'],\n  fetcher: FeedApi.getFeed,\n  options: InfiniteQueryOptions\u003CFeedPage, String>(\n    initialPageParam: '', // '' = posts les plus récents\n    getNextPageParam: (last, _) => last.nextCursor, // posts plus anciens\n    getPreviousPageParam: (first, _) => first.previousCursor, // non nul uniquement après une éviction\n    maxPages: 3, // 3 pour voir l'éviction rapidement ; 8 à 10 en production\n    baseOptions: const QoraOptions(\n      staleTime: Duration(minutes: 2),\n      retryCount: 3,\n    ),\n  ),\n  builder: (context, state, controller) => FeedList(state: state, controller: controller),\n)\n","lib/features/feed/ui/feed_screen.dart","dart",[361,519,520,543,570,589,610,628,659,689,706,722,746,758,766,774,819],{"__ignoreMap":496},[521,522,525,528,532,535,538,540],"span",{"class":523,"line":524},"line",1,[521,526,118],{"class":527},"sBMFI",[521,529,531],{"class":530},"sTEyZ","\u003C",[521,533,534],{"class":527},"FeedPage",[521,536,537],{"class":530},", ",[521,539,506],{"class":527},[521,541,542],{"class":530},">(\n",[521,544,546,549,553,557,560,564,567],{"class":523,"line":545},2,[521,547,548],{"class":530},"  queryKey",[521,550,552],{"class":551},"sMK4o",":",[521,554,556],{"class":555},"spNyl"," const",[521,558,559],{"class":530}," [",[521,561,563],{"class":562},"sfazB","'feed'",[521,565,566],{"class":530},"]",[521,568,569],{"class":551},",\n",[521,571,573,576,578,581,584,587],{"class":523,"line":572},3,[521,574,575],{"class":530},"  fetcher",[521,577,552],{"class":551},[521,579,580],{"class":527}," FeedApi",[521,582,583],{"class":551},".",[521,585,586],{"class":530},"getFeed",[521,588,569],{"class":551},[521,590,592,595,597,600,602,604,606,608],{"class":523,"line":591},4,[521,593,594],{"class":530},"  options",[521,596,552],{"class":551},[521,598,599],{"class":527}," InfiniteQueryOptions",[521,601,531],{"class":530},[521,603,534],{"class":527},[521,605,537],{"class":530},[521,607,506],{"class":527},[521,609,542],{"class":530},[521,611,613,616,618,621,624],{"class":523,"line":612},5,[521,614,615],{"class":530},"    initialPageParam",[521,617,552],{"class":551},[521,619,620],{"class":562}," ''",[521,622,623],{"class":551},",",[521,625,627],{"class":626},"sHwdD"," // '' = posts les plus récents\n",[521,629,631,634,636,639,641,644,647,650,652,654,656],{"class":523,"line":630},6,[521,632,633],{"class":530},"    getNextPageParam",[521,635,552],{"class":551},[521,637,638],{"class":530}," (last",[521,640,623],{"class":551},[521,642,643],{"class":530}," _) ",[521,645,646],{"class":551},"=>",[521,648,649],{"class":530}," last",[521,651,583],{"class":551},[521,653,510],{"class":530},[521,655,623],{"class":551},[521,657,658],{"class":626}," // posts plus anciens\n",[521,660,662,665,667,670,672,674,676,679,681,684,686],{"class":523,"line":661},7,[521,663,664],{"class":530},"    getPreviousPageParam",[521,666,552],{"class":551},[521,668,669],{"class":530}," (first",[521,671,623],{"class":551},[521,673,643],{"class":530},[521,675,646],{"class":551},[521,677,678],{"class":530}," first",[521,680,583],{"class":551},[521,682,683],{"class":530},"previousCursor",[521,685,623],{"class":551},[521,687,688],{"class":626}," // non nul uniquement après une éviction\n",[521,690,692,695,697,701,703],{"class":523,"line":691},8,[521,693,694],{"class":530},"    maxPages",[521,696,552],{"class":551},[521,698,700],{"class":699},"sbssI"," 3",[521,702,623],{"class":551},[521,704,705],{"class":626}," // 3 pour voir l'éviction rapidement ; 8 à 10 en production\n",[521,707,709,712,714,716,719],{"class":523,"line":708},9,[521,710,711],{"class":530},"    baseOptions",[521,713,552],{"class":551},[521,715,556],{"class":555},[521,717,718],{"class":527}," QoraOptions",[521,720,721],{"class":530},"(\n",[521,723,725,728,730,733,736,738,741,744],{"class":523,"line":724},10,[521,726,727],{"class":530},"      staleTime",[521,729,552],{"class":551},[521,731,732],{"class":527}," Duration",[521,734,735],{"class":530},"(minutes",[521,737,552],{"class":551},[521,739,740],{"class":699}," 2",[521,742,743],{"class":530},")",[521,745,569],{"class":551},[521,747,749,752,754,756],{"class":523,"line":748},11,[521,750,751],{"class":530},"      retryCount",[521,753,552],{"class":551},[521,755,700],{"class":699},[521,757,569],{"class":551},[521,759,761,764],{"class":523,"line":760},12,[521,762,763],{"class":530},"    )",[521,765,569],{"class":551},[521,767,769,772],{"class":523,"line":768},13,[521,770,771],{"class":530},"  )",[521,773,569],{"class":551},[521,775,777,780,782,785,787,790,792,795,797,800,803,805,807,809,812,814,817],{"class":523,"line":776},14,[521,778,779],{"class":530},"  builder",[521,781,552],{"class":551},[521,783,784],{"class":530}," (context",[521,786,623],{"class":551},[521,788,789],{"class":530}," state",[521,791,623],{"class":551},[521,793,794],{"class":530}," controller) ",[521,796,646],{"class":551},[521,798,799],{"class":527}," FeedList",[521,801,802],{"class":530},"(state",[521,804,552],{"class":551},[521,806,789],{"class":530},[521,808,623],{"class":551},[521,810,811],{"class":530}," controller",[521,813,552],{"class":551},[521,815,816],{"class":530}," controller)",[521,818,569],{"class":551},[521,820,822],{"class":523,"line":821},15,[521,823,824],{"class":530},")\n",[353,826,827,829,830,833],{},[361,828,370],{}," borne l'",[361,831,832],{},"InfiniteData"," gardé en mémoire. L'exemple utilise 3 pour que l'éviction soit visible après quelques pages ; à 20 posts par page, un feed de production utiliserait plutôt 8 à 10 (160 à 200 posts).",[373,835],{},[348,837,839],{"id":838},"détails-dimplémentation","Détails d'implémentation",[841,842,844],"h3",{"id":843},"pagination-fenêtrée-défiler-dans-le-temps","Pagination fenêtrée : défiler dans le temps",[353,846,847,848,851,852,855,856,858,859,862,863,866,867,869,870,873,874,583],{},"Quand l'utilisateur défile assez bas pour charger la page 4, ",[361,849,850],{},"maxPages: 3"," provoque la ",[356,853,854],{},"suppression"," de la page 1 (les posts les plus récents) depuis ",[361,857,832],{},". Le ",[361,860,861],{},"hasPreviousPage"," de l'état devient ",[361,864,865],{},"true",". Si l'utilisateur défile à nouveau vers le haut, le ",[361,868,434],{}," le détecte et appelle ",[361,871,872],{},"controller.fetchPreviousPage()",", qui refetche la page supprimée en utilisant ",[361,875,876],{},"getPreviousPageParam",[353,878,879,880,460,883,885,886,889],{},"Ce mécanisme complet: suppression, détection et refetch est pourquoi ",[356,881,882],{},"la pagination par curseur est obligatoire",[361,884,370],{},". Les numéros de page entiers se briseraient ici : si 3 nouveaux posts étaient insérés pendant le scroll, refetcher \"page 1\" avec ",[361,887,888],{},"?page=1&limit=20"," retournerait des posts différents du premier chargement. Les curseurs ancrent à une position stable côté serveur.",[490,891,893],{"className":514,"code":892,"filename":516,"language":517,"meta":496,"style":496},"void _onScroll() {\n  final pos = _scrollController.position;\n\n  // Déclencher la page suivante quand à moins de 300px du bas\n  if (pos.extentAfter \u003C 300 &&\n      _latestState case InfiniteSuccess(:final hasNextPage, :final isFetchingNextPage)\n          when hasNextPage && !isFetchingNextPage) {\n    controller.fetchNextPage();\n  }\n\n  // Déclencher la page précédente quand à moins de 300px du haut\n  // (pertinent uniquement après qu'une éviction maxPages a supprimé la page 1)\n  if (pos.extentBefore \u003C 300 &&\n      _latestState case InfiniteSuccess(:final hasPreviousPage, :final isFetchingPreviousPage)\n          when hasPreviousPage && !isFetchingPreviousPage) {\n    controller.fetchPreviousPage();\n  }\n}\n",[361,894,895,907,929,935,940,962,994,1011,1025,1030,1034,1039,1044,1061,1087,1101,1114,1119],{"__ignoreMap":496},[521,896,897,900,904],{"class":523,"line":524},[521,898,899],{"class":555},"void",[521,901,903],{"class":902},"s2Zo4"," _onScroll",[521,905,906],{"class":530},"() {\n",[521,908,909,912,915,918,921,923,926],{"class":523,"line":545},[521,910,911],{"class":555},"  final",[521,913,914],{"class":530}," pos ",[521,916,917],{"class":551},"=",[521,919,920],{"class":530}," _scrollController",[521,922,583],{"class":551},[521,924,925],{"class":530},"position",[521,927,928],{"class":551},";\n",[521,930,931],{"class":523,"line":572},[521,932,934],{"emptyLinePlaceholder":933},true,"\n",[521,936,937],{"class":523,"line":591},[521,938,939],{"class":626},"  // Déclencher la page suivante quand à moins de 300px du bas\n",[521,941,942,946,949,951,954,956,959],{"class":523,"line":612},[521,943,945],{"class":944},"s7zQu","  if",[521,947,948],{"class":530}," (pos",[521,950,583],{"class":551},[521,952,953],{"class":530},"extentAfter ",[521,955,531],{"class":551},[521,957,958],{"class":699}," 300",[521,960,961],{"class":551}," &&\n",[521,963,964,967,970,973,976,978,981,984,986,989,991],{"class":523,"line":630},[521,965,966],{"class":530},"      _latestState ",[521,968,969],{"class":944},"case",[521,971,972],{"class":527}," InfiniteSuccess",[521,974,975],{"class":530},"(",[521,977,552],{"class":551},[521,979,980],{"class":555},"final",[521,982,983],{"class":530}," hasNextPage",[521,985,623],{"class":551},[521,987,988],{"class":551}," :",[521,990,980],{"class":555},[521,992,993],{"class":530}," isFetchingNextPage)\n",[521,995,996,999,1002,1005,1008],{"class":523,"line":661},[521,997,998],{"class":944},"          when",[521,1000,1001],{"class":530}," hasNextPage ",[521,1003,1004],{"class":551},"&&",[521,1006,1007],{"class":551}," !",[521,1009,1010],{"class":530},"isFetchingNextPage) {\n",[521,1012,1013,1016,1018,1020,1023],{"class":523,"line":691},[521,1014,1015],{"class":530},"    controller",[521,1017,583],{"class":551},[521,1019,392],{"class":902},[521,1021,1022],{"class":530},"()",[521,1024,928],{"class":551},[521,1026,1027],{"class":523,"line":708},[521,1028,1029],{"class":530},"  }\n",[521,1031,1032],{"class":523,"line":724},[521,1033,934],{"emptyLinePlaceholder":933},[521,1035,1036],{"class":523,"line":748},[521,1037,1038],{"class":626},"  // Déclencher la page précédente quand à moins de 300px du haut\n",[521,1040,1041],{"class":523,"line":760},[521,1042,1043],{"class":626},"  // (pertinent uniquement après qu'une éviction maxPages a supprimé la page 1)\n",[521,1045,1046,1048,1050,1052,1055,1057,1059],{"class":523,"line":768},[521,1047,945],{"class":944},[521,1049,948],{"class":530},[521,1051,583],{"class":551},[521,1053,1054],{"class":530},"extentBefore ",[521,1056,531],{"class":551},[521,1058,958],{"class":699},[521,1060,961],{"class":551},[521,1062,1063,1065,1067,1069,1071,1073,1075,1078,1080,1082,1084],{"class":523,"line":776},[521,1064,966],{"class":530},[521,1066,969],{"class":944},[521,1068,972],{"class":527},[521,1070,975],{"class":530},[521,1072,552],{"class":551},[521,1074,980],{"class":555},[521,1076,1077],{"class":530}," hasPreviousPage",[521,1079,623],{"class":551},[521,1081,988],{"class":551},[521,1083,980],{"class":555},[521,1085,1086],{"class":530}," isFetchingPreviousPage)\n",[521,1088,1089,1091,1094,1096,1098],{"class":523,"line":821},[521,1090,998],{"class":944},[521,1092,1093],{"class":530}," hasPreviousPage ",[521,1095,1004],{"class":551},[521,1097,1007],{"class":551},[521,1099,1100],{"class":530},"isFetchingPreviousPage) {\n",[521,1102,1104,1106,1108,1110,1112],{"class":523,"line":1103},16,[521,1105,1015],{"class":530},[521,1107,583],{"class":551},[521,1109,396],{"class":902},[521,1111,1022],{"class":530},[521,1113,928],{"class":551},[521,1115,1117],{"class":523,"line":1116},17,[521,1118,1029],{"class":530},[521,1120,1122],{"class":523,"line":1121},18,[521,1123,1124],{"class":530},"}\n",[841,1126,1128,1129],{"id":1127},"création-de-post-optimiste-avec-setinfinitequerydata","Création de post optimiste avec ",[361,1130,443],{},[353,1132,1133,1134,1136],{},"Quand l'utilisateur crée un nouveau post, il doit apparaître en haut du feed instantanément, avant la réponse du serveur. Qora expose ",[361,1135,443],{}," pour écrire directement dans le cache infini :",[490,1138,1141],{"className":514,"code":1139,"filename":1140,"language":517,"meta":496,"style":496},"Future\u003Cvoid> _publish(String content) async {\n  final client = context.qora;\n  const feedKey = ['feed'];\n\n  // 1. Snapshot pour le rollback\n  final snapshot = client.getInfiniteQueryData\u003CFeedPage, String?>(feedKey);\n\n  // 2. Préposition optimiste à la première page\n  if (snapshot != null) {\n    client.setInfiniteQueryData\u003CFeedPage, String?>(\n      feedKey,\n      InfiniteData(\n        pages: [\n          FeedPage(\n            posts: [\n              Post.optimistic(\n                id: 'temp-${DateTime.now().millisecondsSinceEpoch}',\n                content: content,\n                author: currentUser,\n              ),\n              ...snapshot.pages.first.posts,\n            ],\n            nextCursor: snapshot.pages.first.nextCursor,\n            previousCursor: null, // le post optimiste n'a pas encore de curseur stable\n          ),\n          ...snapshot.pages.skip(1),\n        ],\n        pageParams: snapshot.pageParams,\n      ),\n    );\n  }\n\n  try {\n    // 3. Déclencher la vraie mutation\n    await api.createPost(content);\n\n    // 4. Invalider : remplacer l'élément optimiste par la réponse serveur\n    //    refetch() refetche toutes les pages courantes en préservant le nombre de pages\n    await controller.refetch();\n  } catch (error) {\n    // 5. Rollback : restaurer le snapshot exact pré-mutation\n    if (snapshot != null) {\n      client.setInfiniteQueryData(feedKey, snapshot);\n    }\n    _showError(error);\n  }\n}\n","lib/features/feed/ui/compose_sheet.dart",[361,1142,1143,1171,1190,1208,1212,1217,1247,1251,1256,1272,1292,1299,1306,1316,1323,1332,1344,1380,1392,1405,1413,1439,1447,1472,1486,1494,1520,1528,1545,1553,1560,1565,1570,1578,1584,1603,1608,1614,1620,1636,1648,1654,1668,1688,1694,1705,1710],{"__ignoreMap":496},[521,1144,1145,1148,1150,1152,1155,1158,1160,1162,1165,1168],{"class":523,"line":524},[521,1146,1147],{"class":527},"Future",[521,1149,531],{"class":530},[521,1151,899],{"class":555},[521,1153,1154],{"class":530},"> ",[521,1156,1157],{"class":902},"_publish",[521,1159,975],{"class":530},[521,1161,506],{"class":527},[521,1163,1164],{"class":530}," content) ",[521,1166,1167],{"class":944},"async",[521,1169,1170],{"class":530}," {\n",[521,1172,1173,1175,1178,1180,1183,1185,1188],{"class":523,"line":545},[521,1174,911],{"class":555},[521,1176,1177],{"class":530}," client ",[521,1179,917],{"class":551},[521,1181,1182],{"class":530}," context",[521,1184,583],{"class":551},[521,1186,1187],{"class":530},"qora",[521,1189,928],{"class":551},[521,1191,1192,1195,1198,1200,1202,1204,1206],{"class":523,"line":572},[521,1193,1194],{"class":555},"  const",[521,1196,1197],{"class":530}," feedKey ",[521,1199,917],{"class":551},[521,1201,559],{"class":530},[521,1203,563],{"class":562},[521,1205,566],{"class":530},[521,1207,928],{"class":551},[521,1209,1210],{"class":523,"line":591},[521,1211,934],{"emptyLinePlaceholder":933},[521,1213,1214],{"class":523,"line":612},[521,1215,1216],{"class":626},"  // 1. Snapshot pour le rollback\n",[521,1218,1219,1221,1224,1226,1229,1231,1234,1236,1238,1240,1242,1245],{"class":523,"line":630},[521,1220,911],{"class":555},[521,1222,1223],{"class":530}," snapshot ",[521,1225,917],{"class":551},[521,1227,1228],{"class":530}," client",[521,1230,583],{"class":551},[521,1232,1233],{"class":902},"getInfiniteQueryData",[521,1235,531],{"class":530},[521,1237,534],{"class":527},[521,1239,537],{"class":530},[521,1241,506],{"class":527},[521,1243,1244],{"class":530},"?>(feedKey)",[521,1246,928],{"class":551},[521,1248,1249],{"class":523,"line":661},[521,1250,934],{"emptyLinePlaceholder":933},[521,1252,1253],{"class":523,"line":691},[521,1254,1255],{"class":626},"  // 2. Préposition optimiste à la première page\n",[521,1257,1258,1260,1263,1266,1269],{"class":523,"line":708},[521,1259,945],{"class":944},[521,1261,1262],{"class":530}," (snapshot ",[521,1264,1265],{"class":551},"!=",[521,1267,1268],{"class":551}," null",[521,1270,1271],{"class":530},") {\n",[521,1273,1274,1277,1279,1281,1283,1285,1287,1289],{"class":523,"line":724},[521,1275,1276],{"class":530},"    client",[521,1278,583],{"class":551},[521,1280,443],{"class":902},[521,1282,531],{"class":530},[521,1284,534],{"class":527},[521,1286,537],{"class":530},[521,1288,506],{"class":527},[521,1290,1291],{"class":530},"?>(\n",[521,1293,1294,1297],{"class":523,"line":748},[521,1295,1296],{"class":530},"      feedKey",[521,1298,569],{"class":551},[521,1300,1301,1304],{"class":523,"line":760},[521,1302,1303],{"class":527},"      InfiniteData",[521,1305,721],{"class":530},[521,1307,1308,1311,1313],{"class":523,"line":768},[521,1309,1310],{"class":530},"        pages",[521,1312,552],{"class":551},[521,1314,1315],{"class":530}," [\n",[521,1317,1318,1321],{"class":523,"line":776},[521,1319,1320],{"class":527},"          FeedPage",[521,1322,721],{"class":530},[521,1324,1325,1328,1330],{"class":523,"line":821},[521,1326,1327],{"class":530},"            posts",[521,1329,552],{"class":551},[521,1331,1315],{"class":530},[521,1333,1334,1337,1339,1342],{"class":523,"line":1103},[521,1335,1336],{"class":527},"              Post",[521,1338,583],{"class":551},[521,1340,1341],{"class":902},"optimistic",[521,1343,721],{"class":530},[521,1345,1346,1349,1351,1354,1357,1360,1362,1365,1368,1372,1375,1378],{"class":523,"line":1116},[521,1347,1348],{"class":530},"                id",[521,1350,552],{"class":551},[521,1352,1353],{"class":562}," 'temp-",[521,1355,1356],{"class":530},"${",[521,1358,1359],{"class":527},"DateTime",[521,1361,583],{"class":530},[521,1363,1364],{"class":902},"now",[521,1366,1367],{"class":530},"().",[521,1369,1371],{"class":1370},"sHdIc","millisecondsSinceEpoch",[521,1373,1374],{"class":530},"}",[521,1376,1377],{"class":562},"'",[521,1379,569],{"class":551},[521,1381,1382,1385,1387,1390],{"class":523,"line":1121},[521,1383,1384],{"class":530},"                content",[521,1386,552],{"class":551},[521,1388,1389],{"class":530}," content",[521,1391,569],{"class":551},[521,1393,1395,1398,1400,1403],{"class":523,"line":1394},19,[521,1396,1397],{"class":530},"                author",[521,1399,552],{"class":551},[521,1401,1402],{"class":530}," currentUser",[521,1404,569],{"class":551},[521,1406,1408,1411],{"class":523,"line":1407},20,[521,1409,1410],{"class":530},"              )",[521,1412,569],{"class":551},[521,1414,1416,1419,1422,1424,1427,1429,1432,1434,1437],{"class":523,"line":1415},21,[521,1417,1418],{"class":551},"              ...",[521,1420,1421],{"class":530},"snapshot",[521,1423,583],{"class":551},[521,1425,1426],{"class":530},"pages",[521,1428,583],{"class":551},[521,1430,1431],{"class":530},"first",[521,1433,583],{"class":551},[521,1435,1436],{"class":530},"posts",[521,1438,569],{"class":551},[521,1440,1442,1445],{"class":523,"line":1441},22,[521,1443,1444],{"class":530},"            ]",[521,1446,569],{"class":551},[521,1448,1450,1453,1455,1458,1460,1462,1464,1466,1468,1470],{"class":523,"line":1449},23,[521,1451,1452],{"class":530},"            nextCursor",[521,1454,552],{"class":551},[521,1456,1457],{"class":530}," snapshot",[521,1459,583],{"class":551},[521,1461,1426],{"class":530},[521,1463,583],{"class":551},[521,1465,1431],{"class":530},[521,1467,583],{"class":551},[521,1469,510],{"class":530},[521,1471,569],{"class":551},[521,1473,1475,1478,1480,1483],{"class":523,"line":1474},24,[521,1476,1477],{"class":530},"            previousCursor",[521,1479,552],{"class":551},[521,1481,1482],{"class":551}," null,",[521,1484,1485],{"class":626}," // le post optimiste n'a pas encore de curseur stable\n",[521,1487,1489,1492],{"class":523,"line":1488},25,[521,1490,1491],{"class":530},"          )",[521,1493,569],{"class":551},[521,1495,1497,1500,1502,1504,1506,1508,1511,1513,1516,1518],{"class":523,"line":1496},26,[521,1498,1499],{"class":551},"          ...",[521,1501,1421],{"class":530},[521,1503,583],{"class":551},[521,1505,1426],{"class":530},[521,1507,583],{"class":551},[521,1509,1510],{"class":902},"skip",[521,1512,975],{"class":530},[521,1514,1515],{"class":699},"1",[521,1517,743],{"class":530},[521,1519,569],{"class":551},[521,1521,1523,1526],{"class":523,"line":1522},27,[521,1524,1525],{"class":530},"        ]",[521,1527,569],{"class":551},[521,1529,1531,1534,1536,1538,1540,1543],{"class":523,"line":1530},28,[521,1532,1533],{"class":530},"        pageParams",[521,1535,552],{"class":551},[521,1537,1457],{"class":530},[521,1539,583],{"class":551},[521,1541,1542],{"class":530},"pageParams",[521,1544,569],{"class":551},[521,1546,1548,1551],{"class":523,"line":1547},29,[521,1549,1550],{"class":530},"      )",[521,1552,569],{"class":551},[521,1554,1556,1558],{"class":523,"line":1555},30,[521,1557,763],{"class":530},[521,1559,928],{"class":551},[521,1561,1563],{"class":523,"line":1562},31,[521,1564,1029],{"class":530},[521,1566,1568],{"class":523,"line":1567},32,[521,1569,934],{"emptyLinePlaceholder":933},[521,1571,1573,1576],{"class":523,"line":1572},33,[521,1574,1575],{"class":944},"  try",[521,1577,1170],{"class":530},[521,1579,1581],{"class":523,"line":1580},34,[521,1582,1583],{"class":626},"    // 3. Déclencher la vraie mutation\n",[521,1585,1587,1590,1593,1595,1598,1601],{"class":523,"line":1586},35,[521,1588,1589],{"class":944},"    await",[521,1591,1592],{"class":530}," api",[521,1594,583],{"class":551},[521,1596,1597],{"class":902},"createPost",[521,1599,1600],{"class":530},"(content)",[521,1602,928],{"class":551},[521,1604,1606],{"class":523,"line":1605},36,[521,1607,934],{"emptyLinePlaceholder":933},[521,1609,1611],{"class":523,"line":1610},37,[521,1612,1613],{"class":626},"    // 4. Invalider : remplacer l'élément optimiste par la réponse serveur\n",[521,1615,1617],{"class":523,"line":1616},38,[521,1618,1619],{"class":626},"    //    refetch() refetche toutes les pages courantes en préservant le nombre de pages\n",[521,1621,1623,1625,1627,1629,1632,1634],{"class":523,"line":1622},39,[521,1624,1589],{"class":944},[521,1626,811],{"class":530},[521,1628,583],{"class":551},[521,1630,1631],{"class":902},"refetch",[521,1633,1022],{"class":530},[521,1635,928],{"class":551},[521,1637,1639,1642,1645],{"class":523,"line":1638},40,[521,1640,1641],{"class":530},"  } ",[521,1643,1644],{"class":944},"catch",[521,1646,1647],{"class":530}," (error) {\n",[521,1649,1651],{"class":523,"line":1650},41,[521,1652,1653],{"class":626},"    // 5. Rollback : restaurer le snapshot exact pré-mutation\n",[521,1655,1657,1660,1662,1664,1666],{"class":523,"line":1656},42,[521,1658,1659],{"class":944},"    if",[521,1661,1262],{"class":530},[521,1663,1265],{"class":551},[521,1665,1268],{"class":551},[521,1667,1271],{"class":530},[521,1669,1671,1674,1676,1678,1681,1683,1686],{"class":523,"line":1670},43,[521,1672,1673],{"class":530},"      client",[521,1675,583],{"class":551},[521,1677,443],{"class":902},[521,1679,1680],{"class":530},"(feedKey",[521,1682,623],{"class":551},[521,1684,1685],{"class":530}," snapshot)",[521,1687,928],{"class":551},[521,1689,1691],{"class":523,"line":1690},44,[521,1692,1693],{"class":530},"    }\n",[521,1695,1697,1700,1703],{"class":523,"line":1696},45,[521,1698,1699],{"class":902},"    _showError",[521,1701,1702],{"class":530},"(error)",[521,1704,928],{"class":551},[521,1706,1708],{"class":523,"line":1707},46,[521,1709,1029],{"class":530},[521,1711,1713],{"class":523,"line":1712},47,[521,1714,1124],{"class":530},[353,1716,1717,1718,1721,1722,1725,1726,1729,1730,1732,1733,1735,1736,1739],{},"Le détail critique est ",[356,1719,1720],{},"l'étape 4"," : appeler ",[361,1723,1724],{},"controller.refetch()"," plutôt que ",[361,1727,1728],{},"invalidateInfiniteQuery",". ",[361,1731,451],{}," refetche toutes les pages actuellement chargées (8 pages × 20 éléments = 160 posts revalidés en séquence), ce qui remplace le post optimiste avec ID temporaire par le vrai objet serveur. ",[361,1734,1728],{}," réinitialiserait l'état à ",[361,1737,1738],{},"InfiniteInitial"," et recommencerait depuis la page 1, perdant la position de scroll de l'utilisateur.",[841,1741,1743,1745],{"id":1742},"infinitefailure-avec-dégradation-gracieuse",[361,1744,459],{}," avec dégradation gracieuse",[353,1747,1748,1749,1751,1752,1754,1755,1757],{},"Les erreurs de chargement supplémentaire ne doivent jamais effacer le feed déjà chargé. ",[361,1750,459],{}," préserve ",[361,1753,463],{},": le dernier ",[361,1756,832],{}," réussi, exactement pour ce cas :",[490,1759,1761],{"className":514,"code":1760,"filename":516,"language":517,"meta":496,"style":496},"case InfiniteFailure(:final error, :final previousData):\n  if (previousData != null) {\n    final posts = previousData.flatten((p) => p.posts);\n    return SliverList.builder(\n      itemCount: posts.length + 1, // +1 pour le footer d'erreur\n      itemBuilder: (context, i) {\n        if (i \u003C posts.length) return PostTile(post: posts[i]);\n        // Footer d'erreur tout en bas\n        return _LoadMoreErrorBanner(\n          error: error,\n          onRetry: controller.fetchNextPage,\n        );\n      },\n    );\n  }\n  // Aucune donnée du tout : écran d'erreur complet\n  return SliverFillRemaining(child: ErrorScreen(error));\n",[361,1762,1763,1791,1804,1837,1852,1878,1892,1925,1930,1940,1951,1966,1973,1980,1986,1990,1995],{"__ignoreMap":496},[521,1764,1765,1767,1770,1772,1774,1776,1779,1781,1783,1785,1788],{"class":523,"line":524},[521,1766,969],{"class":944},[521,1768,1769],{"class":527}," InfiniteFailure",[521,1771,975],{"class":530},[521,1773,552],{"class":551},[521,1775,980],{"class":555},[521,1777,1778],{"class":530}," error",[521,1780,623],{"class":551},[521,1782,988],{"class":551},[521,1784,980],{"class":555},[521,1786,1787],{"class":530}," previousData)",[521,1789,1790],{"class":551},":\n",[521,1792,1793,1795,1798,1800,1802],{"class":523,"line":545},[521,1794,945],{"class":944},[521,1796,1797],{"class":530}," (previousData ",[521,1799,1265],{"class":551},[521,1801,1268],{"class":551},[521,1803,1271],{"class":530},[521,1805,1806,1809,1812,1814,1817,1819,1822,1825,1827,1830,1832,1835],{"class":523,"line":572},[521,1807,1808],{"class":555},"    final",[521,1810,1811],{"class":530}," posts ",[521,1813,917],{"class":551},[521,1815,1816],{"class":530}," previousData",[521,1818,583],{"class":551},[521,1820,1821],{"class":902},"flatten",[521,1823,1824],{"class":530},"((p) ",[521,1826,646],{"class":551},[521,1828,1829],{"class":530}," p",[521,1831,583],{"class":551},[521,1833,1834],{"class":530},"posts)",[521,1836,928],{"class":551},[521,1838,1839,1842,1845,1847,1850],{"class":523,"line":591},[521,1840,1841],{"class":944},"    return",[521,1843,1844],{"class":527}," SliverList",[521,1846,583],{"class":551},[521,1848,1849],{"class":902},"builder",[521,1851,721],{"class":530},[521,1853,1854,1857,1859,1862,1864,1867,1870,1873,1875],{"class":523,"line":612},[521,1855,1856],{"class":530},"      itemCount",[521,1858,552],{"class":551},[521,1860,1861],{"class":530}," posts",[521,1863,583],{"class":551},[521,1865,1866],{"class":530},"length ",[521,1868,1869],{"class":551},"+",[521,1871,1872],{"class":699}," 1",[521,1874,623],{"class":551},[521,1876,1877],{"class":626}," // +1 pour le footer d'erreur\n",[521,1879,1880,1883,1885,1887,1889],{"class":523,"line":630},[521,1881,1882],{"class":530},"      itemBuilder",[521,1884,552],{"class":551},[521,1886,784],{"class":530},[521,1888,623],{"class":551},[521,1890,1891],{"class":530}," i) {\n",[521,1893,1894,1897,1900,1902,1904,1906,1909,1912,1915,1918,1920,1923],{"class":523,"line":661},[521,1895,1896],{"class":944},"        if",[521,1898,1899],{"class":530}," (i ",[521,1901,531],{"class":551},[521,1903,1861],{"class":530},[521,1905,583],{"class":551},[521,1907,1908],{"class":530},"length) ",[521,1910,1911],{"class":944},"return",[521,1913,1914],{"class":527}," PostTile",[521,1916,1917],{"class":530},"(post",[521,1919,552],{"class":551},[521,1921,1922],{"class":530}," posts[i])",[521,1924,928],{"class":551},[521,1926,1927],{"class":523,"line":691},[521,1928,1929],{"class":626},"        // Footer d'erreur tout en bas\n",[521,1931,1932,1935,1938],{"class":523,"line":708},[521,1933,1934],{"class":944},"        return",[521,1936,1937],{"class":527}," _LoadMoreErrorBanner",[521,1939,721],{"class":530},[521,1941,1942,1945,1947,1949],{"class":523,"line":724},[521,1943,1944],{"class":530},"          error",[521,1946,552],{"class":551},[521,1948,1778],{"class":530},[521,1950,569],{"class":551},[521,1952,1953,1956,1958,1960,1962,1964],{"class":523,"line":748},[521,1954,1955],{"class":530},"          onRetry",[521,1957,552],{"class":551},[521,1959,811],{"class":530},[521,1961,583],{"class":551},[521,1963,392],{"class":530},[521,1965,569],{"class":551},[521,1967,1968,1971],{"class":523,"line":760},[521,1969,1970],{"class":530},"        )",[521,1972,928],{"class":551},[521,1974,1975,1978],{"class":523,"line":768},[521,1976,1977],{"class":530},"      }",[521,1979,569],{"class":551},[521,1981,1982,1984],{"class":523,"line":776},[521,1983,763],{"class":530},[521,1985,928],{"class":551},[521,1987,1988],{"class":523,"line":821},[521,1989,1029],{"class":530},[521,1991,1992],{"class":523,"line":1103},[521,1993,1994],{"class":626},"  // Aucune donnée du tout : écran d'erreur complet\n",[521,1996,1997,2000,2003,2006,2008,2011,2014],{"class":523,"line":1116},[521,1998,1999],{"class":944},"  return",[521,2001,2002],{"class":527}," SliverFillRemaining",[521,2004,2005],{"class":530},"(child",[521,2007,552],{"class":551},[521,2009,2010],{"class":527}," ErrorScreen",[521,2012,2013],{"class":530},"(error))",[521,2015,928],{"class":551},[841,2017,2019,2020],{"id":2018},"lappel-flatten","L'appel ",[361,2021,1821],{},[353,2023,2024,2027,2028,2031,2032,2034],{},[361,2025,2026],{},"InfiniteData\u003CFeedPage, String?>"," contient une ",[361,2029,2030],{},"List\u003CFeedPage>",": un élément par page chargée. ",[361,2033,1821],{}," extrait la liste interne de chaque page et les concatène :",[490,2036,2038],{"className":514,"code":2037,"filename":516,"language":517,"meta":496,"style":496},"case InfiniteSuccess(:final data, :final hasNextPage, :final isFetchingNextPage):\n  final posts = data.flatten((page) => page.posts); // List\u003CPost>\n\n  return SliverList.builder(\n    itemCount: posts.length + 1,\n    itemBuilder: (context, index) {\n      if (index \u003C posts.length) return PostTile(post: posts[index]);\n\n      // Footer : spinner, déclencheur \"charger plus\" ou message fin de feed\n      if (isFetchingNextPage) return const _PageLoader();\n      if (hasNextPage) return _LoadMoreButton(onTap: controller.fetchNextPage);\n      return const _EndOfFeedBanner();\n    },\n  );\n",[361,2039,2040,2074,2106,2110,2122,2141,2155,2184,2188,2193,2211,2237,2251,2258],{"__ignoreMap":496},[521,2041,2042,2044,2046,2048,2050,2052,2055,2057,2059,2061,2063,2065,2067,2069,2072],{"class":523,"line":524},[521,2043,969],{"class":944},[521,2045,972],{"class":527},[521,2047,975],{"class":530},[521,2049,552],{"class":551},[521,2051,980],{"class":555},[521,2053,2054],{"class":530}," data",[521,2056,623],{"class":551},[521,2058,988],{"class":551},[521,2060,980],{"class":555},[521,2062,983],{"class":530},[521,2064,623],{"class":551},[521,2066,988],{"class":551},[521,2068,980],{"class":555},[521,2070,2071],{"class":530}," isFetchingNextPage)",[521,2073,1790],{"class":551},[521,2075,2076,2078,2080,2082,2084,2086,2088,2091,2093,2096,2098,2100,2103],{"class":523,"line":545},[521,2077,911],{"class":555},[521,2079,1811],{"class":530},[521,2081,917],{"class":551},[521,2083,2054],{"class":530},[521,2085,583],{"class":551},[521,2087,1821],{"class":902},[521,2089,2090],{"class":530},"((page) ",[521,2092,646],{"class":551},[521,2094,2095],{"class":530}," page",[521,2097,583],{"class":551},[521,2099,1834],{"class":530},[521,2101,2102],{"class":551},";",[521,2104,2105],{"class":626}," // List\u003CPost>\n",[521,2107,2108],{"class":523,"line":572},[521,2109,934],{"emptyLinePlaceholder":933},[521,2111,2112,2114,2116,2118,2120],{"class":523,"line":591},[521,2113,1999],{"class":944},[521,2115,1844],{"class":527},[521,2117,583],{"class":551},[521,2119,1849],{"class":902},[521,2121,721],{"class":530},[521,2123,2124,2127,2129,2131,2133,2135,2137,2139],{"class":523,"line":612},[521,2125,2126],{"class":530},"    itemCount",[521,2128,552],{"class":551},[521,2130,1861],{"class":530},[521,2132,583],{"class":551},[521,2134,1866],{"class":530},[521,2136,1869],{"class":551},[521,2138,1872],{"class":699},[521,2140,569],{"class":551},[521,2142,2143,2146,2148,2150,2152],{"class":523,"line":630},[521,2144,2145],{"class":530},"    itemBuilder",[521,2147,552],{"class":551},[521,2149,784],{"class":530},[521,2151,623],{"class":551},[521,2153,2154],{"class":530}," index) {\n",[521,2156,2157,2160,2163,2165,2167,2169,2171,2173,2175,2177,2179,2182],{"class":523,"line":661},[521,2158,2159],{"class":944},"      if",[521,2161,2162],{"class":530}," (index ",[521,2164,531],{"class":551},[521,2166,1861],{"class":530},[521,2168,583],{"class":551},[521,2170,1908],{"class":530},[521,2172,1911],{"class":944},[521,2174,1914],{"class":527},[521,2176,1917],{"class":530},[521,2178,552],{"class":551},[521,2180,2181],{"class":530}," posts[index])",[521,2183,928],{"class":551},[521,2185,2186],{"class":523,"line":691},[521,2187,934],{"emptyLinePlaceholder":933},[521,2189,2190],{"class":523,"line":708},[521,2191,2192],{"class":626},"      // Footer : spinner, déclencheur \"charger plus\" ou message fin de feed\n",[521,2194,2195,2197,2200,2202,2204,2207,2209],{"class":523,"line":724},[521,2196,2159],{"class":944},[521,2198,2199],{"class":530}," (isFetchingNextPage) ",[521,2201,1911],{"class":944},[521,2203,556],{"class":555},[521,2205,2206],{"class":527}," _PageLoader",[521,2208,1022],{"class":530},[521,2210,928],{"class":551},[521,2212,2213,2215,2218,2220,2223,2226,2228,2230,2232,2235],{"class":523,"line":748},[521,2214,2159],{"class":944},[521,2216,2217],{"class":530}," (hasNextPage) ",[521,2219,1911],{"class":944},[521,2221,2222],{"class":527}," _LoadMoreButton",[521,2224,2225],{"class":530},"(onTap",[521,2227,552],{"class":551},[521,2229,811],{"class":530},[521,2231,583],{"class":551},[521,2233,2234],{"class":530},"fetchNextPage)",[521,2236,928],{"class":551},[521,2238,2239,2242,2244,2247,2249],{"class":523,"line":760},[521,2240,2241],{"class":944},"      return",[521,2243,556],{"class":555},[521,2245,2246],{"class":527}," _EndOfFeedBanner",[521,2248,1022],{"class":530},[521,2250,928],{"class":551},[521,2252,2253,2256],{"class":523,"line":768},[521,2254,2255],{"class":530},"    }",[521,2257,569],{"class":551},[521,2259,2260,2262],{"class":523,"line":776},[521,2261,771],{"class":530},[521,2263,928],{"class":551},[373,2265],{},[348,2267,2269],{"id":2268},"code-source","Code source",[490,2271,2274],{"className":2272,"code":2273,"language":495,"meta":496},[493],"examples/ex_03_infinite_scroll/\n├── lib/\n│   ├── main.dart                              ← configuration QoraClient + QoraScope\n│   ├── features/\n│   │   └── feed/\n│   │       ├── data/\n│   │       │   └── feed_api.dart              ← appels HTTP par curseur\n│   │       ├── models/\n│   │       │   ├── feed_page.dart             ← { posts, nextCursor, previousCursor }\n│   │       │   └── post.dart                  ← constructeur Post.optimistic()\n│   │       └── ui/\n│   │           ├── feed_screen.dart           ← InfiniteQueryBuilder + logique de scroll\n│   │           ├── post_tile.dart             ← widget post individuel\n│   │           ├── compose_sheet.dart         ← création de post optimiste\n│   │           └── widgets/\n│   │               ├── page_loader.dart       ← spinner en bas de liste\n│   │               ├── load_more_error.dart   ← bannière de réessai en cas d'échec de page\n│   │               └── end_of_feed.dart       ← bannière \"Vous êtes à jour\"\n└── pubspec.yaml\n",[361,2275,2273],{"__ignoreMap":496},[353,2277,2278],{},[2279,2280,2284],"a",{"href":2281,"rel":2282},"https://github.com/meragix/qora/tree/main/examples/ex_03_infinite_scroll",[2283],"nofollow","Voir le code source complet",[373,2286],{},[348,2288,2290],{"id":2289},"notes-de-performance","Notes de performance",[2292,2293,2294,2307],"table",{},[2295,2296,2297],"thead",{},[2298,2299,2300,2304],"tr",{},[2301,2302,2303],"th",{},"Problème",[2301,2305,2306],{},"Solution",[2308,2309,2310,2321,2334,2342,2356],"tbody",{},[2298,2311,2312,2316],{},[2313,2314,2315],"td",{},"Débordement mémoire lors de longues sessions",[2313,2317,2318,2320],{},[361,2319,370],{}," plafonne la fenêtre de pages ; les pages évincées sont refetchées à la demande",[2298,2322,2323,2326],{},[2313,2324,2325],{},"Jank de scroll sur de grandes listes",[2313,2327,2328,2330,2331,2333],{},[361,2329,484],{}," ne rend que les éléments visibles ; l'objet ",[361,2332,832],{}," lui-même est borné",[2298,2335,2336,2339],{},[2313,2337,2338],{},"Dérive de curseur lors d'insertions serveur",[2313,2340,2341],{},"La pagination par curseur ancre chaque page à une position stable côté serveur",[2298,2343,2344,2349],{},[2313,2345,2346,2347],{},"Thundering-herd sur ",[361,2348,451],{},[2313,2350,2351,2352,2355],{},"Les pages sont refetchées ",[356,2353,2354],{},"séquentiellement",", pas en parallèle",[2298,2357,2358,2361],{},[2313,2359,2360],{},"Feed périmé au retour sur l'onglet",[2313,2362,2363,2366,2367,2369,2370],{},[361,2364,2365],{},"staleTime: 2 minutes"," déclenche un ",[361,2368,451],{}," en arrière-plan au re-focus de l'onglet via ",[361,2371,2372],{},"FlutterLifecycleManager",[373,2374],{},[348,2376,2378],{"id":2377},"guides-associés","Guides associés",[380,2380,2381,2394,2400,2415],{},[383,2382,2383,2387,2388,537,2390,2393],{},[2279,2384,2386],{"href":2385},"../recipes/infinite-queries","Guide des requêtes infinies",": ",[361,2389,832],{},[361,2391,2392],{},"InfiniteQueryObserver"," en Dart pur, pagination fenêtrée et défilement bidirectionnel",[383,2395,2396,2399],{},[2279,2397,118],{"href":2398},"../flutter-integration/infinite-query-builder",": binding du widget Flutter",[383,2401,2402,2387,2406,537,2409,537,2412],{},[2279,2403,2405],{"href":2404},"../recipes/optimistic-updates","Guide des mises à jour optimistes",[361,2407,2408],{},"setQueryData",[361,2410,2411],{},"getQueryData",[361,2413,2414],{},"restoreQueryData",[383,2416,2417,2421],{},[2279,2418,2420],{"href":2419},"../api-reference/infinite-query","Référence API Infinite Query",": référence complète des méthodes",[2423,2424,2425],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":496,"searchDepth":545,"depth":545,"links":2427},[2428,2429,2430,2431,2440,2441,2442],{"id":350,"depth":545,"text":351},{"id":377,"depth":545,"text":378},{"id":469,"depth":545,"text":470},{"id":838,"depth":545,"text":839,"children":2432},[2433,2434,2436,2438],{"id":843,"depth":572,"text":844},{"id":1127,"depth":572,"text":2435},"Création de post optimiste avec setInfiniteQueryData",{"id":1742,"depth":572,"text":2437},"InfiniteFailure avec dégradation gracieuse",{"id":2018,"depth":572,"text":2439},"L'appel flatten",{"id":2268,"depth":545,"text":2269},{"id":2289,"depth":545,"text":2290},{"id":2377,"depth":545,"text":2378},"Construire un feed de type réseau social haute performance avec pagination par curseur, gestion mémoire fenêtrée et création de post optimiste instantanée.","md",null,{},{"icon":309},{"title":2449,"description":2450},"Étude de cas : Feed à défilement infini - Qora","Défilement infini de niveau production en Flutter avec InfiniteQueryBuilder, pagination par curseur, fenêtrage maxPages et écritures optimistes dans le cache. Aucun débordement mémoire, aucun jank.","lBn6lijFqUVSGFh8xZY5bHdKEHWEeDtTdZnrQJTJJ_Q",[2453,2455],{"title":302,"path":303,"stem":304,"description":2454,"icon":151,"children":-1},"Une implémentation complète de mise à jour optimiste : setQueryData pour un retour UI immédiat, restoreQueryData pour le rollback automatique en cas d'échec, et invalidateWhere pour la réconciliation serveur.",{"title":311,"path":312,"stem":313,"description":2456,"icon":90,"children":-1},"Une application de todos qui continue de fonctionner sans réseau : données sauvegardées au démarrage, requêtes en pause hors-ligne, écritures mises en file et rejouées dans l'ordre à la reconnexion.",1794040411375]