 Flutterの記事を整理し本にしました
 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を使ってアクセスする際はその値を覚えておく必要があります。







