Refresh Indicator が動かない
YUMEMI Flutter Advent Calendar 2023 12日目の記事です
みんな大好きRefreshIndicator(諸説あります)
Refresh Indicator を使ったことのないFlutterエンジニアはあまり居ないと思います。
同様に、なぜか 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 が動作するようになります

しかしなぜ AlwaysScrollableScrollPhysics を設定すると動作するようになるのでしょうか?
深堀してみましょう
Dive in to RefreshIndicator
RefreshIndicator の実装を見てみると、いくつかの条件を満たした場合にのみ、 RefreshIndicator を開始するとあります
条件のひとつにドラッグ中であるかどうか( dragDetails が null でないかどうか)があることがわかります
そういえば先ほど RefreshIndicator が動作しないケースは、スクロールが不要なリスト長でした
試しにリストの要素数を増やしてみましょう

RefreshIndicator が動作するようになりましたね!
dragDetails が必要
以下箇所で受け取った ScrollNotification が、 dragDetails を持っているという条件が、 RefreshIndicator を表示する一つの条件であることがわかりました
次に dragDetails がどこで設定されているか追ってみましょう
先ほど提示した条件式を改めてみてみると、 ScrollNotification が ScrollStartNotification であることも一つの条件でした
ScrollStartNotification が dispatch されるのは DragScrollActivity#dispatchScrollStartNotification が呼ばれた場合のようです
DragScrollActivity が必要
では ScrollActivity に DragScrollActivity が設定されるのはどこなのかとコードを追っていくと、
最終的に Scrollable#setCanDrag にたどり着きます
setCanDrag に true を渡すとドラッグ可能となり、最終的に DragScrollActivity が dispatch されるという仕組みのようです
では setCanDrag に値を渡しているのはどこかと言うと、
それは ScrollPositionWithSingleContext などであり、
ScrollPhysics#shouldAcceptUserOffset が返す値であるわけです
ここで AlwaysScrollableScrollPhysics の実装を見てみましょう
AlwaysScrollableScrollPhysics を渡すことで RefreshIndicator が動作していたのは、
AlwaysScrollableScrollPhysics#shouldAcceptUserOffset が常に true を返しているから、ということがわかりましたね!
ここでネタバラシ
ここまで読んでくださった皆さまにひとつお詫びがあります
実は最初のコード、シンプルに CustomScrollView + SliverList + SliverChildBuilderDelegate を使えばリスト長に依らず RefreshIndicator が動作してくれます
もちろん内部で CustomScrollView を使っている ListView.builder でも問題ありません
ただしウィジェット構成によっては動作しないこともあり得ますし、フレームワークの実装を見てみることで、どういった条件で RefreshIndicator が動作するのかどうかの理解を深めることは、ちょっとした財産になるかと思います
ぜひ RefreshIndicator の内部実装に思いをはせながら、 swipe-to-refresh を楽しんでみてください
Discussion