便利な機能を実装するためのLocalKeyの使い方
ここまで紹介したLocalKeyと異なる役割のキーがPageStorageKeyです。
このキーはValueKeyを継承したクラスでもあるので、もちろん、これまでと同じ役割も担うこともできますが、親ウィジェットが持つ特別な機能の提供を受ける子ウィジェットとしての識別キーとして利用している一例です。
PageStorageKeyの使い方
PageStorageKeyは画面のスクロール位置などを保存しておく場合に利用することができます。例えば、図4のようにListViewを使った時に画面スクロールした状態から、他の画面に遷移し、再度、同じ画面に戻ったときには同じスクロール位置を再現してほしい場合があります。
このような機能を実装する場合には、画面をStackとして画面状態を維持したまま、次の画面を既存の画面の上部に表示するレイヤーとして実装する方法もあります。しかし、そのような画面のすべての状態を維持しなくても単にスクロール位置だけ戻してくれれば問題ないケースもあるはずです。
そのような画面の指定した情報のみを維持するために利用するのがPageStorageKeyです。ただし、それらを実現しているのは図5のように複数のクラスで構成されていて、PageStorageKey自体は機能を利用するためのフラグ的機能とデータを識別するためのキーの役割のみになります。
そして、リスト6がListViewのスクロール位置を保存できるようにした実装例です。
// (1) 最上位のスコープにデータ保存用のインスタンスを作成 final bucket = PageStorageBucket(); class PageStorageKeyPage extends StatelessWidget { const PageStorageKeyPage({super.key}); @override Widget build(BuildContext context) { // (2) PageStorageを使う return PageStorage( bucket: bucket, child: Scaffold( appBar: AppBar(title: const Text('PageStorageKey'), actions: const []), body: ListView.builder( // (3) 保存用のキーを指定 key: const PageStorageKey<String>("list"), itemCount: 20, itemBuilder: (context, index) { return Container( // : (省略) ); }), ), ); } }
今回はサンプルの説明上わかりやすくするために(1)のようにスコープの最上位にインスタンスを作成しています。実際にはこのPageStorageBucketのライフサイクルがデータ保持のライフサイクルになります。
実際はデータを保持してほしいスコープでインスタンスを作成するようにしてください。次に、(2)データを保持するためのPageStorageウィジェットを作成します。
これは、PageStorageBucketが管理されているスコープをBuildContextを通じて見つけるために便宜上必要なウィジェットになります。そして、(3)では、ListViewでスクロール位置を保存するための保存キーとしてPageStorageKeyを指定しています。
このようにPageStorageを使わない場合のListViewとの違いが少なく、また、機能の関係性もわかりにくいためこのコードだけを見ても、よく役割がわかりにくいところがあると思います。
その原因としては、ListViewのようなスクロール可能なウィジェットでは、あらかじめPageStorageKeyとPageStorageがあれば自動的にスクロール位置の保持が可能になるように実装されているためです。
ListViewの他にも、GridViewなども同じことが可能でありそれらはScrollableのリファレンスを見るとPageStorageを利用することでスクロールの位置が保持できるクラスについての詳細を知ることができます。
筆者が感じたBuildContextやKeyの役割と理解
前回と今回のBuildContextとKeyの内容はわかりにくいと感じる部分もあったかと思います。どちらも機能としては難しいことをしていないにもかかわらず、その目的はわかりにくいと感じていました。そこで筆者が感じたもっと深い部分での役割について、主観的ではありますが、参考まで記したいと思います。
筆者が最もFlutterに対して違和感を感じていた部分が、表示機能を持たないWidgetの存在でした。しかし、Contextの役割とKeyの役割を理解する際には、表示機能をもたないWidgetの機能に着目するとその目的がより強く浮かびあがってきたと感じます。
筆者の経験上、UIアプリケーションで生じる難解な不具合の多くが非UI機能部分とUI部分の連携をする際にUI上の制約から生じる表示や更新ライフサイクルと同期がとれず問題が生じるケースがあります。
特にこのような問題は再現することが難しいケースが多く、さらに利用者毎に生じる現象がちょっとだけ異なる特徴もでやすいです。つまり、まれにしか発生しないが、発生してもその原因や条件などがわかりにくいことです。
しかし、ビジネスロジック上のツリー構造やライフサイクルとデザインや装飾などを含めたUI上のツリー構造やライフサイクルは多くの場合一致しません。そのため、多くのUIフレームワークではこの2つの構造をできるだけ疎結合を維持しながら管理できるようにします。
一方、Flutterはこのような疎結合の解決方法ではなく、あえて密に結合させることで問題をはかろうとしていると感じます。つまり問題が起きにくいUIアプリケーションを作成するためには、UIのライフサイクル上で非UI処理も同様に記述できれば、この問題は生じにくくなります。
ただし問題は生じにくくなりますが、非UIの処理が記述しにくくなります。そこでそのギャップを埋めるために利用できるのがBuildContextとKeyだと思いました。
最後に
今回の記事内に説明にはありませんが、サンプルコードにはスクロール位置ではなく、フォームのテキストフィールドに入力した内容を保持するためのコード(MyFormPage.dart)を用意しました。実装も今回の内容をより理解しやすくするためにできるだけ自前で実装を行っています。より理解を深めたいと思う方は、それらのコードも合わせて参照していただけると幸いです。
また、ここまでのFlutterの連載でほぼ全容については説明できたと思います。次回からは、Flutterでの自由な描写方法や筆者が使っている便利なライブラリなどについて紹介したいと思います。