Flutter PageView Préservation de la Position de Défilement
Problème
Les utilisateurs qui font défiler un flux basé sur PageView signalent que la position de défilement se réinitialise ou « revient en arrière » à une page précédente, notamment après des mises à jour d'état provenant de providers (Riverpod, Provider, BLoC) ou des reconstructions de widget.
Contexte / Conditions de Déclenchement
- PageView avec contenu dynamique provenant de la gestion d'état
- Mises à jour d'état Provider/Riverpod causant des reconstructions de widget
- Pagination qui change itemCount
- Changements d'orientation
- Changement d'onglet et retour à PageView
Cause Racine
Quand Flutter reconstruit l'arbre de widget (en raison de changements d'état), PageView peut perdre sa position de défilement si :
- Aucune
PageStorageKeyn'est fournie pour persister l'état - Le PageController est recréé lors des reconstructions
PageController.keepPageest défini surfalse
Note : allowImplicitScrolling concerne la navigation du focus d'accessibilité, PAS la préservation de la position de défilement. Un bug spécifique (#76569) impliquant le focus TextField a été corrigé dans Flutter.
Solution
1. Ajouter PageStorageKey (CORRECTION PRINCIPALE)
PageView.builder(
// Cette clé indique à Flutter de persister l'état de défilement entre les reconstructions
key: const PageStorageKey<String>('my_page_view'),
itemCount: videos.length,
controller: _pageController,
...
)
2. Conserver PageController dans State (pas dans build)
class _MyScreenState extends State<MyScreen> {
// Créer dans initState, PAS dans build()
late PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(); // Créé une seule fois
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Utiliser la même instance de contrôleur
return PageView.builder(
controller: _pageController,
...
);
}
}
3. S'assurer que keepPage est true (par défaut)
_pageController = PageController(
keepPage: true, // C'est la valeur par défaut, mais soyez explicite si nécessaire
initialPage: 0,
);
4. Pour un état complexe : Utiliser le widget PageStorage
PageStorage(
bucket: PageStorageBucket(),
child: PageView.builder(
key: const PageStorageKey<String>('feed'),
...
),
)
Ce que allowImplicitScrolling Fait Réellement
Ceci concerne l'accessibilité, pas le défilement :
false(par défaut) : Le focus d'accessibilité quitte PageView aux limites de la pagetrue: Le focus d'accessibilité se déplace vers la page suivante au lieu de quitter le widget
Cela ne cause PAS de problèmes de position de défilement. Gardez-le sur true pour une meilleure accessibilité.
Vérification
- Faire défiler jusqu'à la page 10+ du flux
- Déclencher une mise à jour d'état (comme l'arrivée de nouvelles données)
- Vérifier que la position de défilement est préservée
- Tourner l'appareil - vérifier que la position est préservée
- Changer d'onglet et revenir - vérifier que la position est préservée
Exemple
Motif complet pour un flux basé sur Riverpod :
class _VideoFeedScreenState extends ConsumerState<VideoFeedScreen> {
late PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController();
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final videos = ref.watch(videoFeedProvider);
return PageView.builder(
key: const PageStorageKey<String>('video_feed'),
controller: _pageController,
itemCount: videos.length,
allowImplicitScrolling: true, // Pour l'accessibilité
itemBuilder: (context, index) => VideoItem(videos[index]),
);
}
}
Notes
- Le problème Riverpod 3.2.0 #4661 à propos des reconstructions de ProviderScope est distinct
- Si vous utilisez des onglets, enveloppez dans
AutomaticKeepAliveClientMixinpour préserver l'état - Pour les très longues listes, envisagez d'utiliser
restorationIdpour la persistance au redémarrage de l'app