Flutter key とは何なのか？

## 👤対象者
- keyって何か知りたい人
- 何とかキーって色々あってわからない

[Key class](https://api.flutter.dev/flutter/foundation/Key-class.html)
キーは、ウィジェット、エレメント、SemanticsNodesの識別子である。

新しいウィジェットは、そのキーが要素に関連付けられた現在のウィジェットのキーと同じ場合にのみ、既存の要素を更新するために使用されます。

## 動画見た方がわかりやすいかも
Flutter公式の動画です。
https://www.youtube.com/watch?v=kn0EOS-ZiIc

Widgetが、　Widget Tree内で動き回るとき、　keyが状態（state）を保持します。
```dart
body: ListView.builder(
        key: const PageStorageKey('scrollableList'),
        itemCount: 20,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('アイテム ${index + 1}'),
            subtitle: Text('これはアイテム ${index + 1} の説明です'),
            leading: const Icon(Icons.star),
            onTap: () {
              debugPrint('アイテム ${index + 1} がタップされました');
            },
          );
        },
      ),
```

keyはユーザーのスクロール位置を保存したりコレクションの変更時に、Stateを保持したりするために使われます。

### keyの使いどき?
実は使うことはあまりない。何かのStateを保持するような類似したWidgetのコレクションを追加、削除、並べ替えするような時が、keyの使いどきです。　

動画では、Todoリストが例でご紹介されます。リストの並び替える場面ですね。ハッカーニュースの例だと、リストビューが再作成された際に、ニュース記事ウィジェットが誤って、古いステートを取得するのを防いでくれます。

そんなことできるんだ...

### Widgetを描画する際に?
keyが裏でどんな働きをしている。StatelessWidgetは、子に対して一式の秩序あるスロットを持っています。Flutterはどのウィジェットにも対応する要素（エレメント）をビルドします。

エレメントツリーはシンプルで、各ウィジェットの型と子エレメントへの参照に関する情報のみを保持しています。エレメントツリーとは、Flutterアプリの骨格です。元のウィジットを参照することにより得られます。行に並んだタイルウィジットの順序を入れ替えると、Flutterはエレメントツリーを調べて骨格構造を照合します。

>行エレメントから始めてその子に移ります。

そのエレメントツリーは、新しいウィジェットが同型か、keyが古いか調べます。もしそうならその参照を新しいウィジェットに更新します。

動画だと、「この場合、ウィジットにkeyがないので、型で調べます。」と言ってます。

StatefulWidgetだと、State（状態）を持っていて、そこに色の状態が格納されている。
動画を見ると、型を使って調べているそうです。

Flutterはエレメントツリーとそれに対応するステートを使って、デバイス上に表示する内容を判断します。

ここまで動画を見てみると、タイルの並び替えのときに、keyを使う例が紹介されてましたね。

>「要するに、keyは、コレクションの中で、主要ウィジェットの順序を変えたり数を変更するときに便利なのです」と、動画の人は解説しております👩

### アプリ用にkeyが必要?
アプリ用に必要どこに配置する？？？
「それは、保持する必要があるウィジェットサブツリーの際上部に設定するのです」

最初に、Stateについて説明したので、それが最初のStatefulWidgetだと思うかもしれまんせね。でもそれは間違っています！

「えっそうなの？」
ここは、動画の5:36あたりを見た方が良い。　

この位置を入れ替えると、エレメントとウィジェットを照合するFlutterのアルゴリズムは、ツリー階層ごとに照合します。この図で孫をグレー表示で消して、１つずつ階層をみていきましょう。
この第１層では一致しています。
![](https://storage.googleapis.com/zenn-user-upload/f2313a747ad0-20240727.png)

第２階層では、タイルエレメントのkeyとウィジェットのkeyとの不一致をFlutterが把握しそのタイルエレメントを解除して、接続を中断しています。
![](https://storage.googleapis.com/zenn-user-upload/2e4ee675f9c1-20240727.png)

6:17の動画のところだと

この例で使用しているkeyはローカルkeyです。つまりウィジェットとエレメントの一致を照合する際に、Flutterはツリー内の特定レベルの中で、一致するkeyだけを探すのです。

第２階層でそのkey値を伴うタイルエレメントを見つけれてないので、新しいものを作成し新しいステートを初期化しています。

keyの種類が多くあり使い分けの解説がありました。Todoリストの場合だと、テキストが値である[ValueKey](https://api.flutter.dev/flutter/foundation/ValueKey-class.html)を使うと良いそうです。

![](https://storage.googleapis.com/zenn-user-upload/3bdce96f4211-20240727.png)

複数の値を持っていて、複雑なときは、[ObjectKey](https://api.flutter.dev/flutter/widgets/ObjectKey-class.html)が最適とのことです。

コレクションに同じ値の複数の値のウィジェットがある場合や各ウィジェットを他と確実に区別したい場合は、　[UniqueKey](https://api.flutter.dev/flutter/foundation/UniqueKey-class.html)が使えます。
色を区別するまでわからないときに使ったりするようですね。

>避けた方が良いことは、keyの中で無作為の数を使うことです。

と動画で言ってますね。

ウィジェットがビルドされるたび新しい無作為の数が生成されフレーム間の一貫性を失ってしまいます。すると最初からkeyを利用する必要はなかったことになります。

動画8:24の
[PageStorageKey](https://api.flutter.dev/flutter/widgets/PageStorageKey-class.html)は、ユーザーのスクロール位置の保存に特化したkeyで、アプリはそれを後で利用するため保持しておくことができます。

動画8:32の
[GlobalKey](https://api.flutter.dev/flutter/widgets/GlobalKey-class.html)には２つの使用方法があります。　

1. アプリ内の階層を問わずステートを保ったままウィジェットに親を変更させられます。あるいはウィジェットツリーの全然別の場所にある別のウィジェットの情報にアクセスするために使われます。

最初の使用例は、２つの異なる画面で同じウィジェットを表示しつつステートをすべて同じに保つという場合です。ここでGlobalKeyを使うのです。

2. ２つ目の使用例は例えばパスワード認証してもそのツリー内の別のウィジェットとステート情報を共有したくない場合です。でもGlobalKeyは多くの場合グローバル変数と似ています。ステートを調べるのにより良い方法として、継承されたウィジェットかreduxやブロックパターンが使えます。

> 要するにウィジェットツリー全域でステートを保持したいときに、keyを使用するのです　!

「知らんかったわ。GoRouterのコードにもそれが関係してたりして...全域で使ってるもんね。」

これがよく起こるときは、同じ型のウィジェットのコレクションを変更するときです。例えばリスト内においてです。保持したいウィジェットツリーの最上部にkeyを配置して、そのウィジェットで保存するデータの型を基づいて、使用するkeyの型を選択しましょう。

[flutter.io](https://flutter.dev/)でもっとドキュメントをご覧になれます。

アプリ作成をお楽しみください。　

長い解説だった💦
keyについてわかってきた気がする...

## 感想
keyを使う場面は..

- 類似したウィジェットのコレクションを追加・削除・並び替えしたい
- ユーザーのスクロール位置を保持しておきたい

おまけで、Dashくんのドラッグ＆ドロップするのをValueKeyを使って作ってみました💚💙
```dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dash ドラッグ＆ドロップ',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const DragDropPage(),
    );
  }
}

class DragDropPage extends StatefulWidget {
  const DragDropPage({super.key});

  @override
  _DragDropPageState createState() => _DragDropPageState();
}

class _DragDropPageState extends State<DragDropPage> {
  bool _isDashDropped = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Dashをドラッグ＆ドロップ')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            _isDashDropped
                ? Container()
                : Draggable<String>(
                    data: 'dash',
                    feedback: const DashWidget(key: ValueKey('dashFeedback')),
                    childWhenDragging: Container(),
                    child: const DashWidget(key: ValueKey('dashTop')),
                  ),
            DragTarget<String>(
              builder: (
                BuildContext context,
                List<dynamic> accepted,
                List<dynamic> rejected,
              ) {
                return Container(
                  height: 150,
                  width: 150,
                  decoration: BoxDecoration(
                    color: _isDashDropped ? Colors.green : Colors.blue,
                    shape: BoxShape.circle,
                  ),
                  child: _isDashDropped
                      ? const Center(
                          child: DashWidget(key: ValueKey('dashBottom')))
                      : const Center(child: Text('ここにドロップ')),
                );
              },
              onWillAcceptWithDetails: (data) => data.offset.dy > 0,
              onAcceptWithDetails: (data) {
                setState(() {
                  _isDashDropped = true;
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

class DashWidget extends StatelessWidget {
  const DashWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 100,
      width: 100,
      child: Image.network(
        'https://media.licdn.com/dms/image/D4D12AQERHFHFBalnoQ/article-cover_image-shrink_720_1280/0/1697273887022?e=2147483647&v=beta&t=0WpZF9Cn8x9HGetmYnKuSmwqv6uxKnn2h5dz3Hoa90g',
        fit: BoxFit.contain,
      ),
    );
  }
}
```

![](https://storage.googleapis.com/zenn-user-upload/1679429d88d9-20240727.png =250x)
![](https://storage.googleapis.com/zenn-user-upload/9188463b6f7f-20240727.png =250x)

### keyの種類
Flutterでの実装の使い所。2019年の日本語の本の解説

|実装|使いどころ|
|----|----|
|[Valuekey](https://api.flutter.dev/flutter/foundation/ValueKey-class.html)|リスト内の項目で一意になる値がある場合、自分で一意の値を設定したい場合、リストの項目が変化する場合|
|[Key](https://api.flutter.dev/flutter/foundation/Key-class.html)|Key(String)は、ValueKeyのfactoryのためValueKeyと同様|
|[ObjectKey](https://api.flutter.dev/flutter/widgets/ObjectKey-class.html)|リスト内の項目で複数の組み合わせで一意になる場合|
|[UniqueKey](https://api.flutter.dev/flutter/foundation/UniqueKey-class.html)|リストの項目が変化する場合で変化しても問題ない場合|
|[PageStorageKey](https://api.flutter.dev/flutter/widgets/PageStorageKey-class.html)|スクロール位置を保存しておき、後で利用したい場合|
|[Globalkey](https://api.flutter.dev/flutter/widgets/GlobalKey-class.html)|異なる画面に同じウィジェットを使用し、すべて同じ状態にしたい場合ある値を他のウィジェットから取得したい場合|

## 使われている例
https://flutter.salon/flutter/scrollcontroller/
https://medium.com/@davidecarizzoni/flutter-bottom-navigation-bar-with-go-router-b6a89eceb481