💭

Flutterの縦スクロールのPageView.builderでRefreshしたい

2023/12/11に公開

FlutterのPageView.builderを使って縦型スクロールの画面を実装しているときに問題に直面しました。
解決策を考えてみたので忘れないうちに書いておきます。

やりたいこと

PageView.builderを使って縦型スクロールの画面において、一番最初のページから更に戻ろうとしたら何か処理を実行したい

赤い画面から更に戻ろうとしたら、何か処理を実行したいです。(pull to refresh的な感じで)
(例えばTikTokだと一番最初の動画から、さらに戻ろうとすると更新処理が走る)

class FeedPage extends StatelessWidget {
  const FeedPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    List<Widget> containers = [
      Container(
        color: Colors.red,
      ),
      Container(
        color: Colors.blue,
      ),
      Container(
        color: Colors.green,
      ),
    ];

    return Scaffold(
      body: PageView.builder(
        scrollDirection: Axis.vertical,
        itemCount: containers.length,
        itemBuilder: (BuildContext context, int index) {
          return containers[index];
        },
      ),
    );
  }
}

直面した問題

indexが0のページから更に戻りたいときって、どうやって検知すれば良いのか?

無理だったこと

  • GestureDetectoronVerticalDrag***を使う
    あらゆる場所に仕込んでみたが、ダメだった。多分イベントがconflictしてる。
    GitHubのissueも検索しましたが、PageViewの中でGestureDetectorを使うのは、なかなか難しそうですね。。

https://github.com/flutter/flutter/issues/68960

https://github.com/flutter/flutter/issues/66498

考えた解決策

  • index: 0のページにダミーのページを入れる
  • pageControllerのlistenerでPageController.pageが1未満になったときを検知して、そこで処理を実行する
    • ダミーのページには遷移させたくないので、すぐにjumpToPage(1)をする
class FeedPage extends HookConsumerWidget {
  const FeedPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    List<Widget> containers = [
      Container(
        color: Colors.red,
      ),
      Container(
        color: Colors.blue,
      ),
      Container(
        color: Colors.green,
      ),
    ];

    Widget dummyContainer = Container(child: Text('dummyContainer'));

    final _pageController = PageController(initialPage: 1);

    useEffect(
      () {
        void listener() {
          if (_pageController.page! < 1) {
            const snackBar = SnackBar(
              content: Text('OnRefresh'),
            );
            ScaffoldMessenger.of(context).showSnackBar(snackBar);
            _pageController.jumpToPage(1);
          }
        }

        _pageController.addListener(listener);

        return () {
          _pageController.removeListener(listener);
        };
      },
      [],
    );

    return Scaffold(
      body: PageView.builder(
        scrollDirection: Axis.vertical,
        controller: _pageController,
        itemCount: containers.length + 1,
        onPageChanged: (value) {
          if (value == 0) {
            return _pageController.jumpToPage(1);
          }
        },
        itemBuilder: (BuildContext context, int index) {
          if (index == 0) {
            return dummyContainer;
          }
          return containers[index - 1];
        },
      ),
    );
  }
}

20240130 追記

  • 良い方法見つけたので追記
  • GestureDetectoronVerticalDrag***の中で、下向きと上向きのドラッグを検知して、上向きにスライドさせるドラッグであれば、pageController経由でスクロールさせて、下向きであれば、処理を走らせるようにすればConflictしない。
class FeedTabPage extends HookConsumerWidget {
  const FeedTabPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final pageController = PageController(initialPage: 0);
    List<Widget> containers = [
      FirstContainer(pageController),
      Container(
        color: Colors.red,
      ),
      Container(
        color: Colors.green,
      ),
    ];

    return Scaffold(
      body: PageView.builder(
        scrollDirection: Axis.vertical,
        controller: pageController,
        itemCount: containers.length,
        itemBuilder: (BuildContext context, int index) {
          return containers[index];
        },
      ),
    );
  }
}

class FirstContainer extends HookConsumerWidget {
  const FirstContainer(this._pageController, {Key? key}) : super(key: key);

  final PageController _pageController;
  
  Widget build(BuildContext context, WidgetRef ref) {
    final dySum = useState<double>(0);
    final isSnackbarShowing = useState<bool>(false);
    return GestureDetector(
      onVerticalDragStart: (details) {
        dySum.value = 0;
      },
      onVerticalDragUpdate: (details) {
        final dy = details.delta.dy;

        if (dy > 0 && !isSnackbarShowing.value) {
          const snackBar = SnackBar(
            content: Text('OnRefresh'),
          );

          ScaffoldMessenger.of(context).showSnackBar(
            snackBar,
          );
          isSnackbarShowing.value = true;

          return;
        }
        dySum.value = dySum.value + -1 * dy;
        _pageController.jumpTo(dySum.value);
      },
      onVerticalDragEnd: (details) {
        isSnackbarShowing.value = false;

        dySum.value = 0;
      },
      child: Container(
        color: Colors.blue,
      ),
    );
  }
}

結果

なにか、他に良い方法があったら教えてください!

Discussion