対象者
- Flutterを使用してアプリ開発を行っているシステムエンジニア
- ページ間での状態管理に関心がある開発者
- ユーザーエクスペリエンスを向上させたいと考えているアプリ開発者
はじめに
Flutterを使ったアプリ開発において、ユーザーがページ間を移動するたびに状態がリセットされてしまうことに悩んでいませんか?スムーズなユーザーエクスペリエンスを提供するためには、ページ間での状態管理が非常に重要です。そこで役立つのがPageStorage
ウィジェットです。
PageStorage というのは、日本語で「ページの記憶」という意味です。Flutterにおいては、ウィジェットの状態をページ間で保持するウィジェットです。そのため、ユーザーが異なるページやタブ間を移動しても、以前の状態を保持することができます。
実際のアプリとしては、ユーザーがスクロールした位置を記憶しておきたいリストビューや、フォーム入力の途中状態を保存しておきたい場合などに、PageStorageを使用してそのような機能を実現することができます。
この記事では、PageStorageの基本から応用、さらには注意点まで、わかりやすく解説します。これを読めば、あなたもアプリの状態管理を効率的に行い、ユーザーに快適な操作感を提供できるようになるでしょう。さあ、一緒にFlutterアプリ開発のスキルをさらに磨きましょう!
PageStorageとは
PageStorageの概要
Flutterでは、アプリケーションの状態を管理することが重要です。特に、ユーザーが複数のページやタブ間を移動する際に、それぞれのページの状態を保持しておくことが求められます。このようなニーズに応えるために、FlutterではPageStorage
というウィジェットが提供されています。PageStorage
は、ウィジェットの状態を保存し、ユーザーがページを離れた後に戻ってきたときに、その状態を復元する役割を果たします。
PageStorageの役割とメリット
PageStorage
の主な役割は、ウィジェットの状態をページ間で保持することです。これにより、以下のようなメリットがあります。
- ユーザーエクスペリエンスの向上: ユーザーがページを離れても、戻ったときに以前の状態が保持されるため、快適な操作感を提供できます。
- データの保持: ユーザーが入力したフォームのデータやスクロール位置など、重要な情報を保持できます。
- 効率的なリソース利用: ページの状態を保持することで、再描画や再計算のコストを削減できます。
PageStorage
はFlutterアプリケーションにおいて、ユーザーの操作性とデータの保持を向上させる重要な役割を果たします。
PageStorageの応用例
複数のタブやページでの使用
Flutterアプリにおいて、複数のタブやページを持つ場合、ユーザーがタブ間を移動したときに各ページの状態を保持することが重要です。PageStorage
ウィジェットを使用すると、複数のページ間でウィジェットの状態を簡単に管理できます。
スクロール位置の保持
リストやグリッドなどのスクロール可能なビューにおいて、ユーザーがスクロールした位置を保持し、ページ間の移動後にその位置を復元することがユーザー体験を向上させます。ScrollablePage
の例では、ListView.builder
を使用してスクロール可能なリストを作成していますが、PageStorageKey
が設定されていないため、スクロール位置は保持されません。PageStorageKey
を設定することで、スクロール位置を保持し、ページ間の移動後に復元できるようになります。
ListView.builder(
key: PageStorageKey('ScrollablePage')
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
),
State内のデータの保持と復元
アプリの状態(State)に含まれるデータ(例えば、カウンターの値)も、PageStorage
を使用して保持し、復元することができます。CountUpPage
の例では、_incrementCounter
メソッドでカウンターを増加させるたびに、その値をPageStorage
に保存しています。そして、didChangeDependencies
メソッドでページが再構築される際に、PageStorage
からカウンターの値を読み込み、復元しています。PageStorageKey
が設定されているページでは、このデータの保持と復元が行われます。
class _CountUpPageState extends State<CountUpPage> {
int _counter = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final previousValue = PageStorage.of(context).readState(context);
if (previousValue != null && previousValue is int) {
_counter = previousValue;
}
}
void _incrementCounter() {
setState(() {
_counter++;
});
PageStorage.of(context).writeState(context, _counter);
}
}
注意点とトラブルシューティング
キーの重要性
PageStorage
を使用する際には、各ウィジェットの状態を一意に識別するためのキーを正しく設定することが非常に重要です。これは、PageStorage
が状態を保存する際に、キーを基にしてデータを識別し、適切なウィジェットに状態を復元するためです。
final PageStorageKey _key1 = PageStorageKey('page1');
final PageStorageKey _key2 = PageStorageKey('page2');
Widget page1 = Page1(key: _key1);
Widget page2 = Page2(key: _key2);
この例では、Page1
とPage2
のウィジェットに異なるPageStorageKey
を割り当てることで、それぞれの状態を正しく保持できます。
パフォーマンスへの影響
PageStorage
を過度に使用すると、アプリケーションのパフォーマンスに影響を与える可能性があります。特に、大量のデータを保存しようとすると、メモリ使用量が増加し、アプリケーションのレスポンスが遅くなることがあります。そのため、必要なデータのみを保存し、不要なデータは保存しないように注意することが重要です。
// 不要な状態を保存しない
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(); // 状態を持たないシンプルなウィジェット
}
}
この例のように、状態を持たないシンプルなウィジェットは、PageStorage
を使用せずに実装することで、パフォーマンスの低下を防ぐことができます。
Q&A
Q1: PageStorageとは何ですか?
A1: PageStorageは、Flutterでウィジェットの状態を保存し、アプリ内の異なるページ間でその状態を保持する役割を果たすウィジェットです。これにより、ユーザーがページを離れても、戻ったときに以前の状態を復元できます。
Q2: PageStorageの主な利用シナリオは何ですか?
A2: PageStorageは、複数のタブやページがあるアプリケーションでよく使用されます。特に、スクロール位置の保持や、ユーザーが入力したフォームのデータなど、状態を保持したいウィジェットに適しています。
Q3: PageStorageを使用する際の注意点は何ですか?
A3: PageStorageを使用する際には、各ウィジェットの状態を一意に識別するためのキーを正しく設定することが重要です。また、大量のデータを保存しすぎるとパフォーマンスに影響を与える可能性があるため、必要なデータのみを保存するように注意が必要です。
まとめ
この記事では、FlutterのPageStorage
ウィジェットについて学びました。PageStorage
は、アプリ内の異なるページ間でウィジェットの状態を保持する役割を果たします。基本的な使い方から応用例まで、様々なシナリオでの使用方法を理解しました。また、キーの重要性やパフォーマンスへの影響など、注意点も確認しました。これにより、ユーザーエクスペリエンスを向上させるための知識を深めることができました。
参考
ソース(main.dartにコピペして動作確認用)
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PageStorage Example',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final PageStorageBucket _bucket = PageStorageBucket();
final _tabs = [
const CountUpPage(title: 'PageStorageKeyなし'),
const CountUpPage(
title: 'PageStorageKeyあり',
key: PageStorageKey('CountUpPage'),
),
const ScrollablePage(title: 'PageStorageKeyなし'),
const ScrollablePage(
title: 'PageStorageKeyあり',
key: PageStorageKey('ScrollablePage'),
),
];
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
appBar: AppBar(
title: const Text('PageStorage Example'),
bottom: TabBar(
tabs: List.generate(
_tabs.length, (index) => Tab(text: 'Page $index'))),
),
body: PageStorage(
bucket: _bucket,
child: TabBarView(children: _tabs),
),
),
);
}
}
class CountUpPage extends StatefulWidget {
const CountUpPage({super.key, required this.title});
final String title;
@override
State<CountUpPage> createState() => _CountUpPageState();
}
class _CountUpPageState extends State<CountUpPage> {
int _counter = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final previousValue = PageStorage.of(context).readState(context);
if (previousValue != null && previousValue is int) {
_counter = previousValue;
}
}
void _incrementCounter() {
setState(() {
_counter++;
});
PageStorage.of(context).writeState(context, _counter);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class ScrollablePage extends StatefulWidget {
const ScrollablePage({required this.title, super.key});
final String title;
@override
_ScrollablePageState createState() => _ScrollablePageState();
}
class _ScrollablePageState extends State<ScrollablePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
),
),
],
),
);
}
}
このFlutterサンプルコードでは、PageStorage
とPageStorageKey
を使用して、タブ間でウィジェットの状態を保持し、復元する方法を示しています。
コードの解説
-
MyAppクラス: アプリケーションのエントリーポイントで、
MaterialApp
ウィジェットを使用して基本的なアプリ設定を行います。 -
MyHomePageクラス: ホームページを表す
StatefulWidget
で、タブバーとタブビューを含むレイアウトを定義します。 -
_MyHomePageStateクラス:
MyHomePage
の状態を管理します。PageStorageBucket
インスタンスを作成し、TabBarView
内の各ページの状態を保持します。 -
CountUpPageクラス: カウンターを表示し、ボタン押下でカウンターを増加させるページです。
PageStorage
を使用してカウンターの値を保存・復元します。 -
_CountUpPageStateクラス:
CountUpPage
の状態を管理します。didChangeDependencies
メソッドでPageStorage
からカウンターの値を読み込み、_incrementCounter
メソッドでカウンターを増加させ、その値をPageStorage
に保存します。 -
ScrollablePageクラス: スクロール可能なリストを表示するページです。
ListView.builder
を使用してリストアイテムを動的に生成します。
PageStorageとPageStorageKeyの役割
-
PageStorage: ウィジェットの状態を保持するコンテナとして機能します。この例では、
PageStorage
ウィジェットを使用してTabBarView
の子ウィジェットの状態を保持します。 -
PageStorageKey: 各ページの状態を一意に識別するためのキーです。
PageStorageKey
が設定されたページ(CountUpPage
とScrollablePage
)では、その状態が保持され、タブ間の移動後に復元されます。
サンプル作成時の気付き
-
BottomNavigationBarの挙動:
IndexedStack
とBottomNavigationBar
を使用したサンプルアプリを作成しましたが、PageStorage
を使用しなくてもスクロール位置が復元されました。PageStorage
は動作しないことを確認してから対処すれば良い気がします。そのため、PageStorage
を使用する前に、実際に動作を確認してみることが重要です。 -
PageStorageKeyの使用: スクロールバーにおいては、
PageStorageKey
を付けるだけでスクロール位置が保持されます。 -
PageStorageウィジェットの省略: このサンプルでは
PageStorage
ウィジェットを明示的に使用していますが、実際には省略しても正常に動作しました。これは、FlutterのウィジェットツリーにデフォルトでPageStorage
が含まれているためだと思われます。PageStorage.of(context)
を実施すると、インスタンスが取得できました。 -
状態変数の復元: スクロール位置は自動的に復元されることがありますが、カウンターのような状態変数は自動的に復元されないため、そのためのコードを書く必要があります。