flutter-pageview-url-routing-reorder-loop

Par divinevideo · divine-mobile

Corrige les boucles de rebuild infinies dans Flutter lors de l'utilisation de PageView avec un routage basé sur l'URL (GoRouter) et une gestion d'état réactive (Riverpod/BLoC). À utiliser quand : (1) des avertissements RAPID REBUILD apparaissent dans la console (20+), (2) des erreurs "setState() or markNeedsBuild() called during build" proviennent d'un ValueListenableBuilder, (3) le PageView de flux vidéo ou de liste est gelé et ne répond pas au swipe, (4) les overlays ou éléments d'interface clignotent rapidement entre visible/invisible, (5) les logs affichent un élément "moved from index X to Y" de façon répétée. Causé par le suivi des éléments par ID et la mise à jour de l'URL lorsqu'une liste réactive se réordonne.

npx skills add https://github.com/divinevideo/divine-mobile --skill flutter-pageview-url-routing-reorder-loop

Flutter PageView + Détection de Réorganisation avec Routage URL en Boucle Infinie

Problème

Quand un PageView est synchronisé bidirectionnellement avec un routage basé sur URL (ex. GoRouter /home/:index) et que la source de données est un provider réactif (Riverpod, stream BLoC), tenter de suivre l'élément actuellement affiché par son ID et mettre à jour l'URL quand l'élément change d'index dans la liste crée une boucle de rétroaction infinie.

Contexte / Conditions déclenchantes

Symptômes :

  • Console affiche des avertissements RAPID REBUILD #42! (le compteur de builds augmente rapidement)
  • Erreurs setState() or markNeedsBuild() called during build depuis ValueListenableBuilder
  • PageView gelé - l'utilisateur fait défiler mais la page rebondit à la même position
  • L'overlay UI (boutons sociaux, contrôles) clignote entre visible et invisible
  • Les logs montrent un motif répétitif : Video X moved from index 4 -> 3, updating URL

Architecture qui déclenche ceci :

  • PageView.builder avec onPageChanged mettant à jour l'URL via context.go('/feed/$index')
  • Index dérivé de l'URL synchronisé au PageController via jumpToPage() pendant le build
  • Source de données réactive (provider Riverpod, stream) pouvant ré-émettre avec éléments réorganisés
  • Suivi d'élément : _currentItemId comparé à la liste pour détecter les « déplacements »

Exemple de l'anti-motif :

// DANS LA MÉTHODE BUILD - crée une boucle de rétroaction !
if (_currentVideoStableId != null && videos.isNotEmpty) {
  final currentVideoIndex = videos.indexWhere(
    (v) => v.stableId == _currentVideoStableId,
  );
  if (currentVideoIndex != -1 && currentVideoIndex != urlIndex) {
    // Ceci déclenche une mise à jour URL -> rebuild -> synchronisation PageController ->
    // onPageChanged -> mise à jour URL -> BOUCLE INFINIE
    context.go('/home/$currentVideoIndex');
  }
}

Solution

Supprimer entièrement la détection de réorganisation par suivi d'éléments. Le PageController doit être la seule source de vérité pour la page que l'utilisateur visionne.

Étape 1 : Supprimer l'état de suivi

// SUPPRIMER ces champs :
// String? _currentVideoStableId;
// bool _urlUpdateScheduled = false;

Étape 2 : Supprimer le bloc de détection de réorganisation

Supprimer tout code dans build() qui :

  • Cherche un ID d'élément suivi dans la liste actuelle
  • Compare l'index trouvé avec l'index d'URL
  • Programme des mises à jour d'URL quand les indices diffèrent

Étape 3 : Garder seulement des motifs de synchronisation unidirectionnelle

Swipe utilisateur -> URL (unidirectionnel) :

onPageChanged: (newIndex) {
  if (newIndex != urlIndex) {
    context.go('/home/$newIndex');
  }
}

Navigation externe -> PageController (unidirectionnel) :

if (urlIndex != _lastUrlIndex) {
  _lastUrlIndex = urlIndex;
  controller.jumpToPage(urlIndex);
}

Ces deux chemins ne créent pas de boucles car :

  • Swipe utilisateur : l'URL se met à jour, le prochain build voit urlIndex correspondant -> pas de sync nécessaire
  • Nav externe : l'URL change, la sync s'exécute, onPageChanged se déclenche mais newIndex == urlIndex -> pas de mise à jour URL

La Boucle de Rétroaction Expliquée

┌─ Provider réactif ré-émet (nouvelles données du serveur) ─┐
│                                                             │
▼                                                             │
Build s'exécute avec la nouvelle liste vidéo                  │
│                                                             │
▼                                                             │
Détection réorganisation : « Vidéo X déplacée idx 4 vers 3 »  │
│                                                             │
▼                                                             │
Programme mise à jour URL : context.go('/home/3')             │
│                                                             │
▼                                                             │
Build s'exécute : urlIndex=3, PageController à page 4         │
│                                                             │
▼                                                             │
Sync : jumpToPage(3)                                          │
│                                                             │
▼                                                             │
onPageChanged(3) se déclenche → context.go('/home/3')         │
│                                                             │
▼                                                             │
Mais PageController était à 4, qui affichait une vidéo        │
différente → _currentVideoStableId ne correspond pas →        │
détecte « move » à nouveau → mise à jour URL vers /home/4    │
│                                                             │
└────────── BOUCLE INFINIE ────────────────────────────────┘

Vérification

Après suppression de la détection de réorganisation :

  1. Plus d'avertissements RAPID REBUILD dans la console
  2. Pas d'erreurs setState() called during build
  3. Balayer entre les pages fonctionne en douceur
  4. Les overlays UI restent visibles sur la page active
  5. Pas de spam de logs « moved from index X to Y »

Notes

  • Pourquoi la détection de réorganisation semble nécessaire : Quand une liste réactive se réorganise (ex. nouveaux éléments ajoutés au début), l'index de page actuel de l'utilisateur pointe vers un élément différent. Cependant, essayer de « suivre » l'élément crée une UX pire (boucle infinie) que rester au même index de page.

  • Alternative si la préservation de position est critique : Au lieu de la détection de réorganisation basée sur URL, stabiliser l'ordre de la liste. Soit :

    • Ne pas réorganiser pendant que l'utilisateur visionne activement (batch updates)
    • Utiliser un tri stable qui préserve les positions relatives des éléments existants
    • Seulement ajouter/append des éléments nouveaux, jamais réorganiser les existants
  • Motif associé : flutter-pageview-implicit-scrolling-snapback traite un problème PageView différent (snap-back au rebuild d'état) mais peut coexister avec cette boucle.

  • Agnostique du framework : Bien que ceci ait été découvert avec GoRouter + Riverpod, le même motif s'applique à toute combinaison PageView + routage URL + état réactif (Navigator 2.0, auto_route, streams BLoC, etc.)

Skills similaires