Flutterの記事を整理し本にしました
- 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
- 今後はこちらを最新化するため、最新情報はこちらをご確認ください
- 10万文字を超える超大作になっています(笑)
はじめに
- いままで何となくにしていたkeyを学びなおしたので、整理して記事にしてみました。
- nullsafeになってからこの問題は起こらないと思ったのですが、変わらずでした。
まとめ
本チャプターは、下記のGoogleDeveloperDaysの情報をベースとしています。
Key
テストやformで度々登場するkeyですが、何となくWidgetを特定、識別するためということはわかると思うのですが、本チャプターではもう少し細かく見ていきます。
まず、keyとは主にElementがWidgetを識別するために使われます。
Elementについて先に理解しておきたい方は、先に開発の上級1:3つのツリー(Widget/Element/RenderObject)をご参照ください
その後、Widgetツリーが構築されると、その裏でElementツリーが作成されます。
Widgetツリーが再構築されると、Widget自体は廃棄&再構築され、Elementは基本的に再利用され、参照を新しいWidgetに向けます。
keyの内部での使われ方
ここで、行の中に色のついたcontainerが2つあるシンプルなアプリがあります。
このアプリをWidgetツリーで表すと以下のようになります。
ここで、変更が行われて、2つのWidgetが交換されること考えます。
この新旧の入れ替えにおいて、Keyが使われています。
変更されたかは以下のような手順で判断します。
- 同じタイプか比較し、異なっていれば更新する
- 同じkeyか比較し、異なっていれば更新する
※ただし、2において、keyは設定しない場合は、nullになっています。
// Widgetクラスの中のcanUpdateメソッド
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
Flutter2になり、null-safeとなっていますが、keyにおいては設定しない場合はnullというデフォルトは変わっていません。
そのため、同じタイプのkeyを設定しないStatefulWidgetを入れ替えると、typeが同じなので更新する必要がないと判断され、描画が更新されません。
そのため、色が交換されてないという直感に合わない動作になります。
ソースコードでの確認
import 'package:flutter/material.dart';
import 'package:hello_world/statefulTile.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, this.title}) : super(key: key);
final String? title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late List<Widget> tiles;
@override
void initState() {
super.initState();
//2つのStatefulWidgetが準備
tiles = [
StatefulTile(),
StatefulTile(),
];
}
// 入れ替え処理
void changeTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
onPressed: changeTiles,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
import 'dart:math';
import 'package:flutter/material.dart';
class StatefulTile extends StatefulWidget {
StatefulTile({Key? key}) : super(key: key);
@override
_StatefulTileState createState() => _StatefulTileState();
}
class _StatefulTileState extends State<StatefulTile> {
Color _color = Colors.black;
var _random = new Random();
// initStateで色を設定する
@override
void initState() {
super.initState();
_color = Color.fromRGBO(
_random.nextInt(256), _random.nextInt(256), _random.nextInt(256), 1);
}
// ビルド内では色は最初に作ったものを使う
@override
Widget build(BuildContext context) {
print("build");
return Container(color: _color, height: 100, width: 100);
}
}
大切なことは、buildそのものは行われているが、Widget-Element間の紐付けが更新されていないという点です。コンソールにはボタンを押すたびにbuild
の文字は出力されますが画面は変わりません。
keyを設定し正しく比較されるようにしてみます。
// 変更分のみ
@override
void initState() {
super.initState();
tiles = [
StatefulTile(key: UniqueKey()),
StatefulTile(key: UniqueKey()),
];
}
keyを設定し紐付けが変わった時に、元のkeyを同じ階層の中でどこに移ったかを探し、可能な限り再利用します(今回の場合はRowの下に交換した2つのWidgetがあるため再利用される)
しかし、間にPaddingなどがはいると、Paddingの下に2つの交換するWidgetがないので、末端のWidgetにKeyをつけると、うまく動作しません。Paddingにkeyをつけると期待通りに動きます。
Keyの種類
Keyには、LocalKeyとGlobalKeyの2種類があります。
- LocalKey(親Widget以下でユニークとなるキー)
- ValueKey:1つの情報から生成するキー
- 数値、文字列など
- ObjectKey:オブジェクトから生成するキー
- 同じ型でもオブジェクトの中身が異なると違うキーになる
- UniqueKey:特定のWidget内でユニークなキー
- PageStorageKey:ページスクロールの場所を持つキー
- ValueKey:1つの情報から生成するキー
- GlobalKey(アプリ内でユニークとなるキー)
- GlobalKey:アプリ内でユニークなキー
keyを使う際は、単純に他と区別するために払い出すのか、特定のWidgetを特定するために払い出すのかを検討する必要があります。
uniquekeyなどはシステムが自動で払い出すので、払い出しは楽なのですが、keyを使ってアクセスする際はその値を覚えておく必要があります。