Flutterのkeyの整理

---
# :book: Flutterの記事を整理し本にしました :book:
https://zenn.dev/kazutxt/books/flutter_practice_introduction

* <font color="red"><b>本稿の記事を含む様々な記事を体系的に整理し本にまとめました</b></font>
* 今後はこちらを最新化するため、<font color="red"><b>最新情報はこちらをご確認くださ</b>い</font>
* 10万文字を超える超大作になっています(笑)

---

# はじめに
* いままで何となくにしていたkeyを学びなおしたので、整理して記事にしてみました。
* nullsafeになってからこの問題は起こらないと思ったのですが、変わらずでした。

## まとめ
* [Flutter系の記事のまとめ](https://qiita.com/kazutxt/items/524acb1c134015682ccf)


本チャプターは、下記のGoogleDeveloperDaysの情報をベースとしています。
* https://www.youtube.com/watch?v=kn0EOS-ZiIc

# Key

テストやformで度々登場するkeyですが、何となくWidgetを特定、識別するためということはわかると思うのですが、本チャプターではもう少し細かく見ていきます。


まず、**keyとは主にElementがWidget**を識別するために使われます。

Elementについて先に理解しておきたい方は、先に[開発の上級1:3つのツリー(Widget/Element/RenderObject)](https://zenn.dev/kazutxt/books/flutter_practice_introduction/viewer/development_expert1)をご参照ください

その後、Widgetツリーが構築されると、その裏でElementツリーが作成されます。

![pic1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/d1d88e66-e814-7298-dd90-60179cc84dd5.png)


Widgetツリーが再構築されると、Widget自体は廃棄&再構築され、Elementは基本的に再利用され、参照を新しいWidgetに向けます。

![pic2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/2719b3eb-eb5a-ee78-55ec-d77968cd7436.png)


## keyの内部での使われ方

ここで、行の中に色のついたcontainerが2つあるシンプルなアプリがあります。

![Screenshot_1618666637.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/157e9218-3461-e669-d056-ff44057a7b32.png)



このアプリをWidgetツリーで表すと以下のようになります。

![pic3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/3219cfac-66d9-631b-3bca-7900a1312a1f.png)


ここで、変更が行われて、2つのWidgetが交換されること考えます。
この新旧の入れ替えにおいて、Keyが使われています。

変更されたかは以下のような手順で判断します。
1. 同じタイプか比較し、異なっていれば更新する
2. 同じkeyか比較し、異なっていれば更新する
※ただし、2において、keyは設定しない場合は、nullになっています。

```dart:framework.dart
// 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が同じなので更新する必要がないと判断され、描画が更新されません。

![pic4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/d268a892-337e-f1ee-35d4-30538c37542c.png)

そのため、色が交換されてないという直感に合わない動作になります。


## ソースコードでの確認

```dart:main.dart
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),
      ),
    );
  }
}
```


```dart:statefulTile.dart
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`の文字は出力されますが画面は変わりません。

![Screenshot_1618666637.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/3e90d7d7-d4b2-d0c8-10da-70b440f9a623.png)





keyを設定し正しく比較されるようにしてみます。

```dart:main.dart
// 変更分のみ
@override
  void initState() {
    super.initState();
    tiles = [
      StatefulTile(key: UniqueKey()),
      StatefulTile(key: UniqueKey()),
    ];
  }
```

![pic5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/00974d41-d696-6f2f-a6fd-cea4ebbd3a1e.png)


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：ページスクロールの場所を持つキー
* GlobalKey(アプリ内でユニークとなるキー)
  * GlobalKey:アプリ内でユニークなキー

keyを使う際は、単純に他と区別するために払い出すのか、特定のWidgetを特定するために払い出すのかを検討する必要があります。
uniquekeyなどはシステムが自動で払い出すので、払い出しは楽なのですが、keyを使ってアクセスする際はその値を覚えておく必要があります。
![Screenshot_1618666637.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/4ed3429d-a97a-3134-2959-10d8ee8f5d03.png)