事象
長めのスクロールをする画面で一番下までスクロールしてから、上に向かってスクロールすると途中でリストのスクロール位置がずれました。
(スクロールの途中で突然違うスクロール位置に飛ばされるような挙動になりました。)
原因
ListViewのキャッシュ領域が足りていなかったことが原因でした。
今回のレイアウトは、ListViewの中に複数のStreamBuilderを内包するレイアウトで、各StreamBuilderがセクションの役割を果たし、それぞれ別のデータをAPIから取得する作りになっていました。
電話帳のような画面をイメージしていただけると分かりやすいかもしれません。(実際の電話帳は各セクション毎にデータを取りに行くようなことはしないと思いますが、レイアウトのイメージとしてお考えください。)
ListView.builder(
itemCount: 4,
itemBuilder: (context, index) {
switch (index) {
case 0:
// セクション1(あ行のデータを取得・リストで表示)
return StreamBuilder();
case 1:
// セクション2(か行のデータを取得・リストで表示)
return StreamBuilder();
case 2:
// セクション3(さ行のデータを取得・リストで表示)
return StreamBuilder();
case 3:
// セクション4(た行のデータを取得・リストで表示)
return StreamBuilder();
default:
return Container();
}
},
);
この画面を一番下までスクロールすると、あ行のリストは画面外へ隠れてしまいます。
この時、キャッシュ領域からもはみ出たあ行のリストのWidgetが破棄されます。
そして、下から上にスクロールするとあ行のリスト(StreamBuilder)が再描画され、データが取得できたタイミングであ行のリストが再表示されるため、突然スクロール位置がずれるような挙動になっていました。
対応
ListViewのcacheExtentを指定して、キャッシュ領域を広げました。
ListView.builder(
// 250.0はRenderAbstractViewport.defaultCacheExtentより
cacheExtent: 250.0 * 2.0,
itemCount: 4,
itemBuilder: (context, index) {
switch (index) {
case 0:
// セクション1(あ行のデータを取得・リストで表示)
return StreamBuilder();
case 1:
// セクション2(か行のデータを取得・リストで表示)
return StreamBuilder();
case 2:
// セクション3(さ行のデータを取得・リストで表示)
return StreamBuilder();
case 3:
// セクション4(た行のデータを取得・リストで表示)
return StreamBuilder();
default:
return Container();
}
},
);
ListViewにはデフォルトで250.0
のキャッシュ領域が確保されています。(RenderAbstractViewport.defaultCacheExtent
に定義されています。)
cacheExtent
にdoubleでピクセル数を指定することで、キャッシュ領域を広げることができます。
RenderAbstractViewport.defaultCacheExtentはprotectedで定義されており、直接アクセスすると警告が表示されてしまうため、今回は数値をベタ書きで指定しました。
今回はリストの最大長が分かっており、デフォルト値の2倍あれば足りると分かっていたため2倍にしましたが、リストが可変の場合は中身の要素を見て計算するのが良いと思います。
参考
https://medium.com/@greg.perry/decode-listview-a0bc4b90f82d
cacheExtentがどのようなフィールドなのか説明されています。
https://api.flutter.dev/flutter/rendering/RenderViewportBase/cacheExtent.html
デフォルト値が存在することをこのドキュメントで知りました。