
こんにちは、 id:numanuma08 です 🍛
flutterでタブを含むUIを実装しているとき、タブ中に配置したScrollViewやListViewのスクロール位置がタブを変更したタイミングでリセットされる現象は有名な現象だと思います。少しググるだけで解決策は出てきますが、最もスマートかつflutterが公式に推奨している方法はPageStorageを使う方法です。
実装方法も簡単でScrollViewやListViewのコンストラクターで一意のPageStorageKeyを与えるだけです。この記事ではなぜPageStorageKeyを設定するとスクロール位置が保持されるのかflutterのソースコードを調査して明らかにします。
スクロール位置の保存と復帰
スクロール位置の保存と復帰処理はScrollPosition内のsaveScrollOffsetおよびrestoreScrollOffsetに実装されています。
https://github.com/flutter/flutter/blob/63062a64432cce03315d6b5196fda7912866eb37/packages/flutter/lib/src/widgets/scroll_position.dart#L397-L400 https://github.com/flutter/flutter/blob/63062a64432cce03315d6b5196fda7912866eb37/packages/flutter/lib/src/widgets/scroll_position.dart#L418-L425
saveScrollOffsetはスクロール終了時に呼ばれるdidEndScrollから呼び出され、restoreScrollOffsetはScrollPositionのコンストラクターから呼び出されます。スクロール終了時位置の保存、復帰処理はScrollControllerのプロパティkeepScrollOffsetにのみ依存していて、PageStorageKeyがセットされているかは関係なく実行されます。
ちなみにスクロール終了を通知するdidEndScrollはbeginActivityというメソッドから呼ばれています。
beginActivityはScrollableがScrollPositionを参照する際に利用しているScrollPositionWithSingleContext内部でスクロール開始時、終了時に呼び出されています。
ここまでのまとめ
スクロール位置の保存と復帰はScrollPositionが行っていて、位置の保存をスクロール終了時で、復帰はWidgetが生成されたときに行っているとわかりました。次はPageStorageを調査します。
PageStorageが何をやっているのか
PageStorageは内部で持っているPageStorageBucketにキーバリュー形式を使って様々なデータを保存しています。
https://github.com/flutter/flutter/blob/63062a64432cce03315d6b5196fda7912866eb37/packages/flutter/lib/src/widgets/page_storage.dart#L257 https://github.com/flutter/flutter/blob/63062a64432cce03315d6b5196fda7912866eb37/packages/flutter/lib/src/widgets/page_storage.dart#L78
PageStorageBucketに保存するwriteStateとデータを取得するreadStateはデータのキー設定がオプショナルパラメーターです。
https://github.com/flutter/flutter/blob/63062a64432cce03315d6b5196fda7912866eb37/packages/flutter/lib/src/widgets/page_storage.dart#L88-L97 https://github.com/flutter/flutter/blob/63062a64432cce03315d6b5196fda7912866eb37/packages/flutter/lib/src/widgets/page_storage.dart#L107-L115
このキーがPageStorageKeyなのですが、未指定時はPageStorageBucketの_allKeysメソッドを使って現在のWidgetに設定されているkeyをチェックします。
PageStorageはofメソッドを使って子Widgetからアクセス可能です。
そして、PageStorageはWidgetツリーのルートへ自動で位置されるため開発者が配置しなくても問題ありません。
ScrollPositionはPageStorage.ofでPageStorageを参照していたため、プログラマーはPageStorageKeyを設定するだけでスクロール位置の保存と復帰を実現できます。
まとめと考察
ScrollPositionとPageStorageのコードを調査してスクロール位置の保存や復帰が行われる仕組みを調査しました。その結果、次のことがわかりました。
- Widgetを作ったときにスクロール位置の復帰が試みられる
- スクロールを終了したときにスクロール位置の保存が行われる
- PageStorageはWidgetツリーのルートへ自動で配置され、利用される
非常に便利で強力な仕組みである反面、考えられる注意事項として次のものが挙げられます
- 独自のPageStorageが親Widgetにあったとき、意図しない動作になるかもしれない
- PageStorage.ofが使う
findAncestorWidgetOfExactTypeは計算量がO(N)なので、Widgetツリーの肥大化によって処理が重たくなるかもしれない
特に2番目のfindAncestorWidgetOfExactTypeはPageStorageKeyを指定していなくても実行されるため、無駄な処理となるかもしれません。スクロール位置を保持しないときは明示的にScrollController.keepScrollPositionをfalseに設定してもいいかもしれませんね。