はじめに
Flutterで時々利用するWidget の Key
についてのまとめです。
Keyが必要な理由と仕組みについて (1回目)
主にKeyとは何か?、そもそもKeyが必要になる理由についてまとめています。
→ Flutter WidgetにKeyが必要な理由, 仕組みについて
Keyの種類一覧と使い方 (2回目) ← 今回!
Keyの種類一覧とそれらの使い方について解説します。
Keyの指定位置について (3回目)
Keyを指定する位置 (Widget) について解説します。正しい位置に設定しないと意図した動作になりません。
→ 現在準備中です。
Keyの種類について
Keyには大きく分けてGlobalKey
とLocalKey
の2グループが存在し、それぞれそれらを継承したいくつかの種類が存在し、用途に合わせて使い分ける必要があります。
- GlobalKey
- GlobalObjectKey
- LabeledGlobalKey
- LocalKey
- UniqueKey
- ValueKey
- ObjectKey
- PageStorageKey
- その他 (おまけ)
- AssetBundleImageKey
利用するKeyのスコープに合わせて、GlobalKeyなのかLocalKeyなのかを決め、その後、用途に合わせて最終的に利用する派生クラスのKeyを選択します。
GlobalKeyカテゴリ
GlobalKey
名前の通り、任意の画面 (ページ) や Widget ツリーの全く別の階層から特定の Widget にアクセスするために利用します。基本的に親 Widget クラス内のメンバ変数などに定義し、StatefulWidget
の Widget に対して利用します。
利用シーン
- 2つの異なる画面で同じ状態のWidgetを表示したい場合
- 他のWidgetから特定のWidgetを参照したい場合
継承関係
Object > Key > GlobalKey
親Widgetから子Widgetへのアクセス例
以下のように、key名.currentState.xxx
で子Widgetにアクセス可能です。
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
final GlobalKey<_AnimatedTextState> _globalKey =
GlobalKey<_AnimatedTextState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
child: _AnimatedText(
key: _globalKey, // GlobalKeyを指定
text: 'Count $_counter ',
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_counter++;
// key.currentState.メソッド名でコール可能
_globalKey.currentState.updateTextWithAnimation('Count $_counter');
},
child: Icon(Icons.add),
),
);
}
}
GlobalObjectKey
GlobalObjectKey
クラスは以下のように実装されており、後述のValueKey
と同じように、同じインスタンスであれば同じものであると見なすためのKeyです。GlobalKey
がグローバルに1Widget毎に識別可能なIDであるのに対し、GlobalObjectKey
はインスタンス毎に同じID (インスタンスが異なれば違うID) としてもう少し効率的に利用するためのKeyです。
また、GlobalObjectKey
とValueKey
との違いは、グローバルなKeyかある Widget以下の階層 (子Widget) で有効なWidgetかの違いです。
@optionalTypeArgs
class GlobalObjectKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
/// Creates a global key that uses [identical] on [value] for its [operator==].
const GlobalObjectKey(this.value) : super.constructor();
/// The object whose identity is used by this key's [operator==].
final Object value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is GlobalObjectKey<T>
&& identical(other.value, value);
}
@override
int get hashCode => identityHashCode(value);
@override
String toString() {
String selfType = objectRuntimeType(this, 'GlobalObjectKey');
// The runtimeType string of a GlobalObjectKey() returns 'GlobalObjectKey<State<StatefulWidget>>'
// because GlobalObjectKey is instantiated to its bounds. To avoid cluttering the output
// we remove the suffix.
const String suffix = '<State<StatefulWidget>>';
if (selfType.endsWith(suffix)) {
selfType = selfType.substring(0, selfType.length - suffix.length);
}
return '[$selfType ${describeIdentity(value)}]';
}
}
継承関係
Object > Key > GlobalKey > GlobalObjectKey
使用例 (参考文献)
Random #Flutter tip:
— Remi Rousselet (@remi_rousselet) January 16, 2020
You can use GlobalKeys inside StatelessWidget by combining GlobalObjectKey and BuildContext (and potentially tuples)
This avoids using global variables, which therefore makes the widget reusable. 🙂 pic.twitter.com/uWs9x8fuIS
LabeledGlobalKey
LabeledGlobalKey
は、GlobalKey
を継承したデバッグ機能を入れ込んだ派生クラスです。
@optionalTypeArgs
class LabeledGlobalKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
/// Creates a global key with a debugging label.
///
/// The label does not affect the key's identity.
// ignore: prefer_const_constructors_in_immutables , never use const for this class
LabeledGlobalKey(this._debugLabel) : super.constructor();
final String _debugLabel;
@override
String toString() {
final String label = _debugLabel != null ? ' $_debugLabel' : '';
if (runtimeType == LabeledGlobalKey)
return '[GlobalKey#${shortHash(this)}$label]';
return '[${describeIdentity(this)}$label]';
}
}
GlobalKey
内部では、以下のようにLabeledGlobalKey
を利用していますが、一般の開発者が利用するものではないと思いますので、覚えなくて良いでしょう。
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
/// Creates a [LabeledGlobalKey], which is a [GlobalKey] with a label used for
/// debugging.
///
/// The label is purely for debugging and not used for comparing the identity
/// of the key.
factory GlobalKey({ String debugLabel }) => LabeledGlobalKey<T>(debugLabel);
継承関係
Object > Key > GlobalKey > LabeledGlobalKey
LocalKeyカテゴリ
LocalKey
GlobalKey
がアプリ内で一意に識別可能なIDであることに対して、LocalKey
は親 Widget にぶら下がる子 Widget 間で一意に識別可能なIDです。
LocalKey
クラスは以下の通り、抽象化クラスのため、Flutter アプリ開発者はこれを単体で利用することはありません。代わりに、これを継承したValueKey
やUniqueKey
, ObjectKey
を利用します。
abstract class LocalKey extends Key {
/// Default constructor, used by subclasses.
const LocalKey() : super.empty();
}
継承関係
Object > Key > LocalKey
LocalKey (ValueKey, UniqueKey, ObjectKey含む) が必要な理由
ListViewなど、動的にWidget構成やエントリー数が変化する場合、Widgetの再構成 (rebuild) が実行されるため、何もしないとそれまでのStateがリセットされてしまいます。その一番分かり易い例を説明したいと思います。
以下のサンプルは、初期状態ではTextField
を3つ用意し、一番上のTextField
をFABのクリックに連動して非表示にする (したい) コードです。
class _MyHomePageState extends State<MyHomePage> {
bool showFirst = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
if (showFirst) MyTextField(), // ここがFABクリックによって非表示に変化
MyTextField(),
MyTextField(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState((){
showFirst = false;
});
},
child: Icon(Icons.add),
),
);
}
}
class MyTextField extends StatefulWidget {
@override
_MyTextFieldState createState() => _MyTextFieldState();
}
class _MyTextFieldState extends State<MyTextField> {
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
);
}
}
しかし、3つのTextField
にテキストを入力した後にFABボタンを押すと、意図した一番上のではなくて、何故か最後のTextField
が非表示になります (なった様に見えます)。
これを解決するために、Key (LocalKey)
が必要なのです。
ソースコードを以下の様に修正します。
class _MyHomePageState extends State<MyHomePage> {
bool showFirst = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
if (showFirst) MyTextField(key: ValueKey(0)), // ← 修正!
MyTextField(key: ValueKey(1)), // ← 修正!
MyTextField(key: ValueKey(2)), // ← 修正!
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState((){
showFirst = false;
});
},
child: Icon(Icons.add),
),
);
}
}
class MyTextField extends StatefulWidget {
MyTextField({Key key}) : super(key: key); // ← 追加!
@override
_MyTextFieldState createState() => _MyTextFieldState();
}
参考文献
UniqueKey
重複も含むエントリー数が多い ListView のように、不特定多数の Widget が存在する場合に利用します。
一意なIDが自動で割り振られますが、それが故に開発者が親 Widget などから意図的にIDを指定して該当 Widgetを操作することが出来ません (生成したKeyインスタンスの一覧をどこかに保存しておけば話は別ですが) 。
一方、後述のValueKey
であれば、毎回newしても引数の情報から同じ Key が生成されるため、ID指定による Widget アクセスが容易です。
使い方
UniqueKey
は以下の様に利用します。
このサンプルだとUniqueKey
を利用するメリットがありませんが…
class _MyHomePageState extends State<MyHomePage> {
bool showFirst = true;
// build(BuildContext context)の中で毎回のnewしたらダメです! 必ず一度だけ
final UniqueKey _key0 = UniqueKey();
final UniqueKey _key1 = UniqueKey();
final UniqueKey _key2 = UniqueKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
if (showFirst) MyTextField(key: _key0),
MyTextField(key: _key1),
MyTextField(key: _key2),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState((){
showFirst = false;
});
},
child: Icon(Icons.add),
),
);
}
}
継承関係
Object > Key > LocalKey > UniqueKey
ValueKey
UniqueKey
とは異なり、エントリー数が少ない固定数の ListView などで開発者が固有のID (文字列や数値等) を明示的に指定する場合に利用します。
UniqueKey
クラスは以下のように実装されており、引数には任意の型を指定可能で、その引数のインスタンスの違いでKeyを識別します。
class ValueKey<T> extends LocalKey {
/// Creates a key that delegates its [operator==] to the given value.
const ValueKey(this.value);
/// The value to which this key delegates its [operator==]
final T value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ValueKey<T>
&& other.value == value;
}
@override
int get hashCode => hashValues(runtimeType, value);
@override
String toString() {
final String valueString = T == String ? "<'$value'>" : '<$value>';
// The crazy on the next line is a workaround for
// https://github.com/dart-lang/sdk/issues/33297
if (runtimeType == _TypeLiteral<ValueKey<T>>().type)
return '[$valueString]';
return '[$T $valueString]';
}
}
使い方
ValueKey
のメリットは、以下のコードの様に毎回newしても必ず一意のIDが作成されることです。
そのため、以下の様に build メソッドの中で毎回newしても問題はありません。毎回newするのは無駄ではありますが…。
class _MyHomePageState extends State<MyHomePage> {
bool showFirst = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
if (showFirst) MyTextField(key: ValueKey('key0')),
MyTextField(key: ValueKey('key1')),
MyTextField(key: ValueKey('key2')),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState((){
showFirst = false;
});
},
child: Icon(Icons.add),
),
);
}
}
利用シーン
- リスト内の項目で一意になる値がある場合
- 自分で一意の値を設定したい場合
- リストの項目が変化する場合
継承関係
Object > Key > LocalKey > ValueKey
ObjectKey
ObjectKey
はObject (WidgetやState等) を引数にして生成する Key です。例えば、ユーザ名やユーザID, 性別, 電話番号など、複数の情報を保有したリストを表示する場合において、各アイテムのWidgetを識別する時などに利用します。
ValueKyeとの違い
ValueKey
が1つの情報で特定できる Key であるのに対して、ObjectKey
は複数の情報で特定できる Key という位置付けです。
ソースコードを見てみる
ObjectKey
のソースコードを見るとわかりやすいです。
ObjectKeyの引数にはObject
を指定し、何の情報でもKeyに指定できるようになっています。また、operator の部分でruntimeType
(クラス名) と identical (インスタンスが同じかどうか比較) で Key の比較を実現していますね。これを見ると、ObjectKey
の多用はパフォーマンス観点では良くないです。可能なら (主キーがあるなら) 、ValueKey
の方が良いでしょう。
class ObjectKey extends LocalKey {
/// Creates a key that uses [identical] on [value] for its [operator==].
const ObjectKey(this.value);
/// The object whose identity is used by this key's [operator==].
final Object value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ObjectKey
&& identical(other.value, value);
}
@override
int get hashCode => hashValues(runtimeType, identityHashCode(value));
@override
String toString() {
if (runtimeType == ObjectKey)
return '[${describeIdentity(value)}]';
return '[${objectRuntimeType(this, 'ObjectKey')} ${describeIdentity(value)}]';
}
}
継承関係
Object > Key > LocalKey > ObjectKey
PageStorageKey
PageStorageKey
はよく出てくる話が、Tab + ListViewの構成時にタブ切り替えてもスクロール位置をキープするために利用するものという内容です。まさにこの通りですね。
仕組み的には、PageStorageKey
は他のLocalKeyとは少し扱いが異なります。ソースコードを見ると何となく用途が分かってくると思います。Flutterはページが切り替わっても、各 Widget の State を保存できる様に、PageStorage
という機能 (クラス) を用意しています。これを利用することで、任意の情報をストアしたりロードしたり出来ます。
タブ操作でページが切り替わり、元のページに戻った時の表示を本来律儀に対応しようとすると、各 Widget にPagestorage
で保存されていた (Page切り替わり時に保存した) 状態の情報をロードしてあげて、スクロール位置を調整するという作業が必要になります。ただ、アプリ開発者がこれを毎回やりたくないですよね?
そこでもっと便利になるようにと用意されたのが、このPageStorageKey
です。これを利用すれば、Key を指定するだけでページが切り替わり前後の状態を自動で対応してくれるため、大変便利です。
使い方
使い方自体は特別特殊なことはなく、ValueKey
やObjectKey
と同じ様に利用します。
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
bottom: TabBar(
tabs: <Widget>[
Tab(
child: Text('Tab-1'),
),
Tab(
child: Text('Tab-2'),
),
],
),
),
body: TabBarView(
children: <Widget>[
// Stateが保存されないパターン
//_TabPage(tab: 0),
//_TabPage(tab: 1),
// PageStorageKeyを利用してStateを保存するパターン
_TabPage(key: PageStorageKey(0), tab: 0),
_TabPage(key: PageStorageKey(1), tab: 1),
],
),
),
);
}
}
class _TabPage extends StatefulWidget {
_TabPage({Key key, this.tab}) : super(key: key);
final int tab;
@override
_TabPageState createState() => _TabPageState();
}
class _TabPageState extends State<_TabPage> {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title:
Text('${widget.tab}: Item $index'),
);
},
);
}
}
継承関係
Object > Key > LocalKey > ValueKey > PageStorageKey
その他カテゴリ
AssetBundleImageKey
その他にあまり使うシーンは無いと思いますが、Key と名が付くものでAssetBundleImageKey
というものがあります。
AssetImage
やExactAssetImage
のアセット系を利用する場合のIDとして利用します。
これは特に覚える必要性もないでしょう。
サンプルコード (の紹介)