flutter-pageview-scroll-position-preservation

Par divinevideo · divine-mobile

Corrigez les problèmes de « snap back » ou de réinitialisation de position de défilement dans un `PageView` Flutter lors de rebuilds provider/state. À utiliser quand : (1) le `PageView` défile puis revient à la page précédente après une mise à jour d'état, (2) la position de défilement se réinitialise lors d'un rebuild Riverpod/Provider, (3) le feed remonte en haut lors d'un rafraîchissement des données, (4) le `PageView` perd sa position après un changement d'orientation ou un rebuild de widget.

npx skills add https://github.com/divinevideo/divine-mobile --skill flutter-pageview-scroll-position-preservation

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 :

  1. Aucune PageStorageKey n'est fournie pour persister l'état
  2. Le PageController est recréé lors des reconstructions
  3. PageController.keepPage est défini sur false

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 page
  • true : 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

  1. Faire défiler jusqu'à la page 10+ du flux
  2. Déclencher une mise à jour d'état (comme l'arrivée de nouvelles données)
  3. Vérifier que la position de défilement est préservée
  4. Tourner l'appareil - vérifier que la position est préservée
  5. 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 AutomaticKeepAliveClientMixin pour préserver l'état
  • Pour les très longues listes, envisagez d'utiliser restorationId pour la persistance au redémarrage de l'app

Références

Skills similaires