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
に設定してもいいかもしれませんね。