ウィジェットの State を保存する PageStorage の使い方
ウィジェットの State (状態) とは?
ステートフル・ウィジェット (StatefulWidget) では、State クラスがウィジェットの状態を保持することになっています。 State に変更を加える時 (あるいは変更した時) に State クラスの setState メソッドを呼ぶことで、 その変更を Flutter フレームワークに通知します。
これにより、フレームワークはウィジェット (サブ) ツリーを再構築し、その結果ユーザーインターフェイスが更新されます。
State が破棄されると、UI の状態もリセットされる
上述のように State は UI の状態を保持するので、フレームワークによって State が破棄されれば、 UI の状態もクリアされてしまいます。
基本的にあるページから次のページに移動したタイミングで State は破棄されるので、通常はそれで問題ないのですが、時に問題が発生する場合もあります。
例えば、次の画面のように TabBarView で、はじめの画面で何か入力して、 ちょっとお隣のページにちょっとスライドして移動して、すぐに戻ってきた時に、もとの画面の入力が消えているのでは、違和感がありますよね。
期待する動作は、次のように元の入力内容が維持されていることでしょう。
ここでは、実際にこの画面を作成して、どのように対策するか具体的に説明します。
最初の画面作り 〜 TabBarView
このように横スライドする画面は TabBar と TabBarView で作ります。
TabBar と TabBarView については次のページも参考にしてください。
アプリケーションのメインのアプリケーションクラスは次の通りです。
main.dart
import 'package:flutter/material.dart';
import './page1.dart';
import './page2.dart';
import './page3.dart';
void main() => runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp(),
),
);
class TabInfo {
String label;
Widget widget;
TabInfo(this.label, this.widget);
}
class MyApp extends StatelessWidget {
final List<TabInfo> _tabs = [
TabInfo("FIRST", Page1()),
TabInfo("SECOND", Page2()),
TabInfo("THIRD", Page3()),
];
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
appBar: AppBar(
title: Text('Tab Controller'),
bottom: PreferredSize(
child: TabBar(
isScrollable: true,
tabs: _tabs.map((TabInfo tab) {
return Tab(text: tab.label);
}).toList(),
),
preferredSize: Size.fromHeight(30.0),
),
),
body: TabBarView(children: _tabs.map((tab) => tab.widget).toList()),
),
);
}
}
page1.dart
ここで Text ウィジェットと IconButton ウィジェットを並べて、 カウンターを表示しています。
import 'package:flutter/material.dart';
class Page1 extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _Page1State();
}
}
class Page1Params {
int counter1 = 0;
int counter2 = 0;
}
class _Page1State extends State<Page1> {
Page1Params _params = Page1Params();
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'${_params.counter1}',
style: TextStyle(
fontSize: 48,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.remove, size: 32.0),
onPressed: () {
setState(() {
_params.counter1--;
});
},
),
IconButton(
icon: Icon(Icons.add, size: 32.0),
onPressed: () {
setState(() {
_params.counter1++;
});
},
),
],
),
Text(
'${_params.counter2}',
style: TextStyle(
fontSize: 48,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.remove, size: 32.0),
onPressed: () {
setState(() {
_params.counter2--;
});
},
),
IconButton(
icon: Icon(Icons.add, size: 32.0),
onPressed: () {
setState(() {
_params.counter2++;
});
},
),
],
)
],
),
);
}
}
page2.dart と page3.dart
Page2 と Page3 は次のようなコードです。ページを移動するために用意しただけなので、何もしないので何でも構いません。
import 'package:flutter/material.dart';
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
Page3 はクラス名を Page3 としてください。
ここまで作り、上で見たように次のような画面になれば OK です。
さて、これをデバッグしていきましょう。
PageStorage の利用方法
ウィジェットの State 保存維持のために、Flutter フレームワークでは PageStorage クラスが用意されています。
使い方は簡単です。ここでは Page1 で PageStorage を使いたいので、次の手順で PageStorage を組み込みます。
1. キーを受け取るコンストラクタを作成
Page1 ウィジェットでコンストラクタを作成して、キーを受け取ります。page1.dart 内の Page1 クラスにコンストラクタを追加します。
class Page1 extends StatefulWidget {
Page1({Key key}) : super(key: key); //この行を追加
@override
State<StatefulWidget> createState() {
return _Page1State();
}
}
受け取ったキーは直ちに super に渡すだけで OK です。これによって、 このキーに関連付けされたストレージが用意されます。
2. Page1 作成時にキーを渡す
main.dart で Page1 を作成する時に、ページストレージキー (PageStorageKey) を渡します。
class MyApp extends StatelessWidget {
final List<TabInfo> _tabs = [
TabInfo(
"FIRST",
Page1(key: PageStorageKey<String>("key_Page1"))
), // ここでキーを渡している
TabInfo("SECOND", Page2()),
TabInfo("THIRD", Page3()),
];
キーに設定する文字列は一意になるようにすれば、何でも構いません。
3. PageStorage を読み取る
page1.dart 内の Page1State クラスで、 didChangeDependencies メソッドをオーバーライドします。
class _Page1State extends State<Page1> {
Page1Params _params;
@override
void didChangeDependencies() { // このメソッドをオーバーライド
Page1Params p = PageStorage.of(context).readState(context);
if (p != null) {
_params = p;
} else {
_params = Page1Params();
}
super.didChangeDependencies();
}
これはステートフルウィジェットのライフサイクルで initState の次に呼びされるメソッドです。 ここで State の初期化をします。
readState メソッドでストレージから読み取りますが、何も書き込まれていなければ null が返ります。
4. PageStorage に書き込む
同じく Page1State クラス内の build メソッドで、 UI の状態を保持するオブジェクト (ここでは Page1Params というオブジェクトにまとめています) の変更後、writeState を呼び出して、 PageStorage に書き込んでいます。
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'${_params.counter1}',
style: TextStyle(
fontSize: 48,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.remove, size: 32.0),
onPressed: () {
setState(() {
_params.counter1--;
});
PageStorage.of(context).writeState(context, _params);
},
),
IconButton(
icon: Icon(Icons.add, size: 32.0),
onPressed: () {
setState(() {
_params.counter1++;
});
PageStorage.of(context).writeState(context, _params);
},
),
],
),
Text(
'${_params.counter2}',
style: TextStyle(
fontSize: 48,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.remove, size: 32.0),
onPressed: () {
setState(() {
_params.counter2--;
});
PageStorage.of(context).writeState(context, _params);
},
),
IconButton(
icon: Icon(Icons.add, size: 32.0),
onPressed: () {
setState(() {
_params.counter2++;
});
PageStorage.of(context).writeState(context, _params);
},
),
],
)
],
),
);
}
これで終わりです。これで次のように状態が維持 (実際は復元) されます。
手順のおさらい
コード例を示したので長々となりましたが、まとめるとステップは次の通りです。
- StatefulWidget クラスのコンストラクタでキーを受け取り super に渡す
- ウィジェット作成時にキーを渡す
- readState でページストレージを読み取る (didChangeDependencies() メソッドのオーバーライド)
- writeState でページストレージに書き込む (随時)
以上で、State を保持する PageStorage の使用方法について説明しました。