私たちは以前に多くのコンポーネントを使用してきました。例えば、Container
、Row
、またはElevatedButton
などの Widget は、使用頻度が比較的高いです。しかし、彼らのコンストラクタには、通常最初の属性として存在する Key があることに気づいていましたか?
Flutter では、Key はユニークなコンポーネントを識別するために使用されます。その名の通り、Key はユニークな識別子として使用されます。
私たちは以前、この Key を使用していませんでした。この場合、2 つの状況が考えられます:
- コンポーネントのタイプが異なる場合:例えば、
Container
とSizedBox
のように、Flutter 内部ではコンポーネントのタイプによって区別できるため、この場合はkeyを使用する必要はありません。 - コンポーネントのタイプが同じ場合、例えば複数の
Container
からなる配列のように、Flutter はコンポーネントのタイプによって複数のコンポーネントを区別できないため、この場合はKeyを使用する必要があります。
デフォルトでは、key が定義されていない場合、Flutter は自動的に key を作成せず、widget のタイプとインデックスを使用して呼び出します。
Key がないことによる問題を事例を通じて説明します:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Widget> list = [];
@override
void initState() {
super.initState();
list = [
const Box(color: Colors.blue),
const Box(color: Colors.yellow),
const Box(color: Colors.red),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.shuffle(); // shuffleはこのリストの要素をランダムに並べ替えます
});
},
child: const Icon(Icons.refresh),
),
);
}
}
class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);
@override
State<Box> createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.transparent,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}
上記は StatefulWidget の例で、floatingActionButton
をクリックすることで画面上の 3 つの Box の順序をランダムに並べ替えます。効果は以下の通りです。
(画像)
見ての通り、並べ替えボタンをクリックしても、box の表面上の順序は変わりますが、内部の State、つまり数字の表示は変わりません。これは Flutter がこれらの 3 つの Box を同じものとして認識し、stateless レベルの変更しか行わないためであり、State 内のものは動かないため、この現象が生じます(説明が不十分ですが、後で補足します)。
この時、私たちは key を使用する必要があります:
Key をユニークな識別子として使用する#
Key は大きく分けて 2 つのカテゴリに分けられます:GlobalKey
とLocalKey
。その名の通り、Global はグローバル、Local はローカル(非グローバル)です。
// GlobalKeyは全体で共有できます
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
// GlobalKeyは非常に高価であり、合理的に使用する必要があります
// LocalKey
// ValueKeyはLocalKeyです
const Box(color: Colors.blue, key: ValueKey(1)),
// ObjectKeyもLocalKeyです
const Box(color: Colors.red, key: ObjectKey(Text("key"))),
// UniqueKeyもLocalKeyで、唯一無二の存在として(ユニークなハッシュコードを生成します)
Box(color: Colors.red, key: UniqueKey()),
// PageStorageKeyは現在のページの状態を保存できます。例えば、リストのスクロール状態など
PageStorageKey()
LocalKey
とGlobalKey
の違い:
LocalKey
は同じ親ノードを持つ比較の場合に適用されます。つまり、1 つの親ノードが複数の子ノードを管理する場合、子ノードはLocalKey
を使用するのが最適です。GlobalKey
は Widget を越えて状態にアクセスでき、複数の親ノードを持つ比較の場合に適用されます。つまり、子ノードが複数の親ノードに対応する場合、例えば後で説明する横縦画面の切り替え機能では、Row
とColumn
の間で切り替える必要があり、LocalKey
だけを使用すると、State
の現在の状態が失われます。
以上がGlobalKey
とLocalKey
の定義方法です。
では、上記の例の Box に Key を追加しましょう:
以下の例では、MediaQuery
のorientation
属性を使用してデバイスの横縦画面状態を確認します
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// GlobalKeyは全体で共有でき、横縦画面の切り替え時にコンポーネントの状態が失われません
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
List<Widget> list = [];
@override
void initState() {
super.initState();
list = [
Box(color: Colors.blue, key: _globalKey1),
Box(color: Colors.yellow, key: _globalKey2),
Box(color: Colors.red, key: _globalKey3),
];
}
// localKey(ValueKey)を使用すると、状態が失われます
// List<Widget> list = [
// const Box(color: Colors.blue, key: ValueKey(1)),
// const Box(color: Colors.yellow, key: ValueKey(2)),
// const Box(color: Colors.red, key: ValueKey(3)),
// ];
@override
Widget build(BuildContext context) {
// 縦画面portrait横画面landscape
print(MediaQuery.of(context).orientation);
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
// 横画面では横向きに表示し、縦画面では縦向きに表示します
// しかし、これを変更すると状態が保存されなくなります。なぜならColumnとRowのkeyやコンポーネント自体が異なるからです
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.shuffle(); // shuffleはこのリストの要素をランダムに並べ替えます
});
},
child: const Icon(Icons.refresh),
),
);
}
}
効果は以下の通りです:
注意:
GlobalKey
の使用はコストが高いため、他の選択肢がある場合はできるだけ避けるべきです。また、同じGlobalKey
は Widget ツリー全体で一意でなければならず、重複してはいけません。
GlobalKey を使用して子要素を操作する#
GlobalKey には非常に重要な特性があります:Widget を越えて State にアクセスできることです。
例えば、子 Widget(Box)内の State を取得したい場合、次のようにします。
var boxState = _globalKey.currentState as _BoxState;
boxState
を取得した後、BoxState
内のメソッドや属性を直接呼び出すことができます。
さらに、子 widget ノードを取得することもできます。
var boxWidget = _globalKey.currentWidget as Box;
これにより、box 内のさまざまな属性、例えば背景色、幅、高さ、フォントサイズなどを取得できます。
完全なデモ:
// 親widget
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// GlobalKeyは全体で共有できます
final GlobalKey _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
child: Box(color: Colors.blue, key: _globalKey),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 親コンポーネントが子要素のstatewidgetの属性を取得します
var boxState = _globalKey.currentState as _BoxState;
setState(() {
boxState._count++;
});
print(boxState._count);
// 子Widgetのメソッドを呼び出します
boxState.run();
// 子widgetを取得します
var boxWidget = _globalKey.currentWidget as Box;
print(boxWidget.color); // MaterialColor(primary value: Color(0xff2196f3))
// 子コンポーネントがレンダリングした属性を取得します
var renderBox =
_globalKey.currentContext!.findRenderObject() as RenderBox;
print(renderBox.size); // Size(100.0, 100.0)
},
child: const Icon(Icons.add),
),
);
}
}
// 子widget
class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);
@override
State<Box> createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
void run() {
print("私はboxのRunメソッドです");
}
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.transparent,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}