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 builddepuisValueListenableBuilder - 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.builderaveconPageChangedmettant à jour l'URL viacontext.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 :
_currentItemIdcomparé à 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 :
- Plus d'avertissements
RAPID REBUILDdans la console - Pas d'erreurs
setState() called during build - Balayer entre les pages fonctionne en douceur
- Les overlays UI restent visibles sur la page active
- 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-snapbacktraite 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.)