🎉

Refresh Indicator が動かない

2023/12/12に公開

YUMEMI Flutter Advent Calendar 2023 12日目の記事です

みんな大好きRefreshIndicator(諸説あります)

Refresh Indicator を使ったことのないFlutterエンジニアはあまり居ないと思います。

同様に、なぜか Refresh Indicator が動作しない事象に遭遇した方もいらっしゃるのではないでしょうか?

なぜか動かない Refresh Indicator

SingleChildScrollView(
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      ...List.generate(
        25,
            (index) => Container(
          padding:
          const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
          child: Text('item $index'),
        ),
      ),
    ],
  ),
);

検索してみると、AlwaysScrollableScrollPhysics を設定するといった解決法をよく見かけます

SingleChildScrollView(
  physics: const AlwaysScrollableScrollPhysics(),
  child: Column(children: [...])
)

確かに AlwaysScrollableScrollPhysics を設定することで、 RefreshIndicator が動作するようになります

Refresh Indicator 動く

しかしなぜ AlwaysScrollableScrollPhysics を設定すると動作するようになるのでしょうか?
深堀してみましょう

Dive in to RefreshIndicator

RefreshIndicator の実装を見てみると、いくつかの条件を満たした場合にのみ、 RefreshIndicator を開始するとあります

https://github.com/flutter/flutter/blob/3.13.9/packages/flutter/lib/src/material/refresh_indicator.dart#L331-L341

条件のひとつにドラッグ中であるかどうか( dragDetails が null でないかどうか)があることがわかります

そういえば先ほど RefreshIndicator が動作しないケースは、スクロールが不要なリスト長でした

試しにリストの要素数を増やしてみましょう

要素数を増やしてみよう

RefreshIndicator が動作するようになりましたね!

dragDetails が必要

以下箇所で受け取った ScrollNotification が、 dragDetails を持っているという条件が、 RefreshIndicator を表示する一つの条件であることがわかりました
https://github.com/flutter/flutter/blob/3.13.9/packages/flutter/lib/src/material/refresh_indicator.dart#L542-L548

次に dragDetails がどこで設定されているか追ってみましょう

先ほど提示した条件式を改めてみてみると、 ScrollNotificationScrollStartNotification であることも一つの条件でした

ScrollStartNotification が dispatch されるのは DragScrollActivity#dispatchScrollStartNotification が呼ばれた場合のようです

https://github.com/flutter/flutter/blob/3.13.9/packages/flutter/lib/src/widgets/scroll_activity.dart#L444

DragScrollActivity が必要

では ScrollActivityDragScrollActivity が設定されるのはどこなのかとコードを追っていくと、
最終的に Scrollable#setCanDrag にたどり着きます

https://github.com/flutter/flutter/blob/3.13.9/packages/flutter/lib/src/widgets/scrollable.dart#L711

setCanDrag に true を渡すとドラッグ可能となり、最終的に DragScrollActivity が dispatch されるという仕組みのようです

では setCanDrag に値を渡しているのはどこかと言うと、
それは ScrollPositionWithSingleContext などであり、
ScrollPhysics#shouldAcceptUserOffset が返す値であるわけです

https://github.com/flutter/flutter/blob/3.13.9/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart#L100-L104

ここで AlwaysScrollableScrollPhysics の実装を見てみましょう

https://github.com/flutter/flutter/blob/3.13.9/packages/flutter/lib/src/widgets/scroll_physics.dart#L925-L954

AlwaysScrollableScrollPhysics を渡すことで RefreshIndicator が動作していたのは、
AlwaysScrollableScrollPhysics#shouldAcceptUserOffset が常に true を返しているから、ということがわかりましたね!

ここでネタバラシ

ここまで読んでくださった皆さまにひとつお詫びがあります

実は最初のコード、シンプルに CustomScrollView + SliverList + SliverChildBuilderDelegate を使えばリスト長に依らず RefreshIndicator が動作してくれます

もちろん内部で CustomScrollView を使っている ListView.builder でも問題ありません

ただしウィジェット構成によっては動作しないこともあり得ますし、フレームワークの実装を見てみることで、どういった条件で RefreshIndicator が動作するのかどうかの理解を深めることは、ちょっとした財産になるかと思います

ぜひ RefreshIndicator の内部実装に思いをはせながら、 swipe-to-refresh を楽しんでみてください

Discussion