Flutter Widget Keyの種類と使い方について

# はじめに

Flutterで時々利用するWidget の [`Key`](https://api.flutter.dev/flutter/foundation/Key-class.html)についてのまとめです。

## Keyが必要な理由と仕組みについて (1回目)
主にKeyとは何か？、そもそもKeyが必要になる理由についてまとめています。
→ [Flutter WidgetにKeyが必要な理由, 仕組みについて](https://qiita.com/kurun_pan/items/f91228cf5c793ec3f3cc)

## Keyの種類一覧と使い方 (2回目)　←　今回！
Keyの種類一覧とそれらの使い方について解説します。

## Keyの指定位置について (3回目)
Keyを指定する位置 (Widget) について解説します。正しい位置に設定しないと意図した動作になりません。
→ 現在準備中です。

# Keyの種類について

Keyには大きく分けて`GlobalKey`と`LocalKey`の2グループが存在し、それぞれそれらを継承したいくつかの種類が存在し、用途に合わせて使い分ける必要があります。

* [GlobalKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#globalkey)
 * [GlobalObjectKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#globalobjectkey)
 * [LabeledGlobalKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#labeledglobalkey)
* [LocalKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#localkey)
 * [UniqueKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#uniquekey)
 * [ValueKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#valuekey)
 * [ObjectKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#objectkey)
 * [PageStorageKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#pagestoragekey)
* その他 (おまけ)
 * [AssetBundleImageKey](https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c#assetbundleimagekey)

利用するKeyのスコープに合わせて、GlobalKeyなのかLocalKeyなのかを決め、その後、用途に合わせて最終的に利用する派生クラスのKeyを選択します。
<img width="1223" alt="スクリーンショット 2020-04-04 14.13.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/0cba5fc9-b84d-d564-5145-489a6b58be48.png">

# GlobalKeyカテゴリ

## [GlobalKey](https://api.flutter.dev/flutter/widgets/GlobalKey-class.html)

名前の通り、任意の画面 (ページ) や Widget ツリーの全く別の階層から特定の Widget にアクセスするために利用します。基本的に親 Widget クラス内のメンバ変数などに定義し、`StatefulWidget`の Widget に対して利用します。

### 利用シーン
* 2つの異なる画面で同じ状態のWidgetを表示したい場合
* 他のWidgetから特定のWidgetを参照したい場合

### 継承関係
Object > Key > GlobalKey

### 親Widgetから子Widgetへのアクセス例

以下のように、`key名.currentState.xxx`で子Widgetにアクセス可能です。

```Dart: 
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  final GlobalKey<_AnimatedTextState> _globalKey =
      GlobalKey<_AnimatedTextState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        child: _AnimatedText(
          key: _globalKey, // GlobalKeyを指定
          text: 'Count $_counter ',
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _counter++;
          // key.currentState.メソッド名でコール可能
          _globalKey.currentState.updateTextWithAnimation('Count $_counter');
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
```

## [GlobalObjectKey](https://api.flutter.dev/flutter/widgets/GlobalObjectKey-class.html)

`GlobalObjectKey`クラスは以下のように実装されており、後述の`ValueKey`と同じように、同じインスタンスであれば同じものであると見なすためのKeyです。`GlobalKey`がグローバルに1Widget毎に識別可能なIDであるのに対し、`GlobalObjectKey`はインスタンス毎に同じID (インスタンスが異なれば違うID) としてもう少し効率的に利用するためのKeyです。

また、`GlobalObjectKey`と`ValueKey`との違いは、グローバルなKeyかある Widget以下の階層 (子Widget) で有効なWidgetかの違いです。

```Dart:
@optionalTypeArgs
class GlobalObjectKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
  /// Creates a global key that uses [identical] on [value] for its [operator==].
  const GlobalObjectKey(this.value) : super.constructor();

  /// The object whose identity is used by this key's [operator==].
  final Object value;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is GlobalObjectKey<T>
        && identical(other.value, value);
  }

  @override
  int get hashCode => identityHashCode(value);

  @override
  String toString() {
    String selfType = objectRuntimeType(this, 'GlobalObjectKey');
    // The runtimeType string of a GlobalObjectKey() returns 'GlobalObjectKey<State<StatefulWidget>>'
    // because GlobalObjectKey is instantiated to its bounds. To avoid cluttering the output
    // we remove the suffix.
    const String suffix = '<State<StatefulWidget>>';
    if (selfType.endsWith(suffix)) {
      selfType = selfType.substring(0, selfType.length - suffix.length);
    }
    return '[$selfType ${describeIdentity(value)}]';
  }
}
```

### 継承関係
Object > Key > GlobalKey<T> > GlobalObjectKey

### 使用例 (参考文献)
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Random <a href="https://twitter.com/hashtag/Flutter?src=hash&amp;ref_src=twsrc%5Etfw">#Flutter</a> tip:<br><br>You can use GlobalKeys inside StatelessWidget by combining GlobalObjectKey and BuildContext (and potentially tuples)<br><br>This avoids using global variables, which therefore makes the widget reusable. 🙂 <a href="https://t.co/uWs9x8fuIS">pic.twitter.com/uWs9x8fuIS</a></p>&mdash; Remi Rousselet (@remi_rousselet) <a href="https://twitter.com/remi_rousselet/status/1217832292334346241?ref_src=twsrc%5Etfw">January 16, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## [LabeledGlobalKey](https://api.flutter.dev/flutter/widgets/LabeledGlobalKey-class.html)

`LabeledGlobalKey`は、`GlobalKey`を継承したデバッグ機能を入れ込んだ派生クラスです。

```Dart:
@optionalTypeArgs
class LabeledGlobalKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
  /// Creates a global key with a debugging label.
  ///
  /// The label does not affect the key's identity.
  // ignore: prefer_const_constructors_in_immutables , never use const for this class
  LabeledGlobalKey(this._debugLabel) : super.constructor();

  final String _debugLabel;

  @override
  String toString() {
    final String label = _debugLabel != null ? ' $_debugLabel' : '';
    if (runtimeType == LabeledGlobalKey)
      return '[GlobalKey#${shortHash(this)}$label]';
    return '[${describeIdentity(this)}$label]';
  }
}
```

`GlobalKey`内部では、以下のように`LabeledGlobalKey`を利用していますが、一般の開発者が利用するものではないと思いますので、覚えなくて良いでしょう。

```Dart:flutter/packages/flutter/lib/src/widgets/framework.dart
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
  /// Creates a [LabeledGlobalKey], which is a [GlobalKey] with a label used for
  /// debugging.
  ///
  /// The label is purely for debugging and not used for comparing the identity
  /// of the key.
  factory GlobalKey({ String debugLabel }) => LabeledGlobalKey<T>(debugLabel);
```

### 継承関係
Object > Key > GlobalKey<T> > LabeledGlobalKey

# LocalKeyカテゴリ

## [LocalKey](https://api.flutter.dev/flutter/foundation/LocalKey-class.html)

`GlobalKey`がアプリ内で一意に識別可能なIDであることに対して、`LocalKey`は親 Widget にぶら下がる子 Widget 間で一意に識別可能なIDです。

`LocalKey`クラスは以下の通り、抽象化クラスのため、Flutter アプリ開発者はこれを単体で利用することはありません。代わりに、これを継承した`ValueKey`や`UniqueKey`, `ObjectKey`を利用します。

```Dart:flutter/packages/flutter/lib/src/foundation/key.dart
abstract class LocalKey extends Key {
  /// Default constructor, used by subclasses.
  const LocalKey() : super.empty();
}
```

### 継承関係
Object > Key > LocalKey

### LocalKey (ValueKey, UniqueKey, ObjectKey含む) が必要な理由

ListViewなど、動的にWidget構成やエントリー数が変化する場合、Widgetの再構成 (rebuild) が実行されるため、何もしないとそれまでのStateがリセットされてしまいます。その一番分かり易い例を説明したいと思います。

以下のサンプルは、初期状態では`TextField`を3つ用意し、<b>一番上</b>の`TextField`をFABのクリックに連動して<b>非表示</b>にする (したい) コードです。

```Dart:ダメな例
class _MyHomePageState extends State<MyHomePage> {
  bool showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          children: <Widget>[
            if (showFirst) MyTextField(), // ここがFABクリックによって非表示に変化
            MyTextField(),
            MyTextField(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState((){
            showFirst = false;
          });
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

class MyTextField extends StatefulWidget {
  @override
  _MyTextFieldState createState() => _MyTextFieldState();
}

class _MyTextFieldState extends State<MyTextField> {
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: controller,
    );
  }
}
```

しかし、3つの`TextField`にテキストを入力した後にFABボタンを押すと、意図した一番上のではなくて、何故か最後の`TextField`が非表示になります (なった様に見えます)。
<img width="300" alt="ezgif.com-video-to-gif.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/8585a738-e8d8-c1dd-e194-cdd02a748957.gif">

これを解決するために、`Key (LocalKey)`が必要なのです。
ソースコードを以下の様に修正します。

```Dart:正しい例
class _MyHomePageState extends State<MyHomePage> {
  bool showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          children: <Widget>[
            if (showFirst) MyTextField(key: ValueKey(0)), // ←　修正！
            MyTextField(key: ValueKey(1)), // ←　修正！
            MyTextField(key: ValueKey(2)), // ←　修正！
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState((){
            showFirst = false;
          });
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

class MyTextField extends StatefulWidget {
  MyTextField({Key key}) : super(key: key); // ←　追加！
  @override
  _MyTextFieldState createState() => _MyTextFieldState();
}
```

意図した通り、動作する様になりました！
<img width="300" alt="ezgif.com-video-to-gif.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/be7e022d-a7c2-ad61-df37-5ab6baf03e1f.gif">

### 参考文献
* [Using Keys in Flutter](https://medium.com/@ayushpguptaapg/using-keys-in-flutter-1c5fb586b2a5)

## [UniqueKey](https://api.flutter.dev/flutter/widgets/UniqueKey-class.html)

重複も含むエントリー数が多い ListView のように、不特定多数の Widget が存在する場合に利用します。

一意なIDが自動で割り振られますが、それが故に開発者が親 Widget などから意図的にIDを指定して該当 Widgetを操作することが出来ません (生成したKeyインスタンスの一覧をどこかに保存しておけば話は別ですが) 。

一方、後述の`ValueKey`であれば、毎回newしても引数の情報から同じ Key が生成されるため、ID指定による Widget アクセスが容易です。

### 使い方
`UniqueKey`は以下の様に利用します。
このサンプルだと`UniqueKey`を利用するメリットがありませんが…

```Dart:使用例
class _MyHomePageState extends State<MyHomePage> {
  bool showFirst = true;

  // build(BuildContext context)の中で毎回のnewしたらダメです！ 必ず一度だけ
  final UniqueKey _key0 = UniqueKey();
  final UniqueKey _key1 = UniqueKey();
  final UniqueKey _key2 = UniqueKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          children: <Widget>[
            if (showFirst) MyTextField(key: _key0),
            MyTextField(key: _key1),
            MyTextField(key: _key2),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState((){
            showFirst = false;
          });
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
```

### 継承関係
Object > Key > LocalKey > UniqueKey

## [ValueKey](https://api.flutter.dev/flutter/foundation/ValueKey-class.html)

`UniqueKey`とは異なり、エントリー数が少ない固定数の ListView などで開発者が固有のID (文字列や数値等) を明示的に指定する場合に利用します。

`UniqueKey`クラスは以下のように実装されており、引数には任意の型を指定可能で、その引数のインスタンスの違いでKeyを識別します。

```Dart:flutter/packages/flutter/lib/src/foundation/key.dart
class ValueKey<T> extends LocalKey {
  /// Creates a key that delegates its [operator==] to the given value.
  const ValueKey(this.value);

  /// The value to which this key delegates its [operator==]
  final T value;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ValueKey<T>
        && other.value == value;
  }

  @override
  int get hashCode => hashValues(runtimeType, value);

  @override
  String toString() {
    final String valueString = T == String ? "<'$value'>" : '<$value>';
    // The crazy on the next line is a workaround for
    // https://github.com/dart-lang/sdk/issues/33297
    if (runtimeType == _TypeLiteral<ValueKey<T>>().type)
      return '[$valueString]';
    return '[$T $valueString]';
  }
}
```

### 使い方
`ValueKey`のメリットは、以下のコードの様に毎回newしても必ず一意のIDが作成されることです。
そのため、以下の様に build メソッドの中で毎回newしても問題はありません。毎回newするのは無駄ではありますが…。

```Dart:使用例
class _MyHomePageState extends State<MyHomePage> {
  bool showFirst = true;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          children: <Widget>[
            if (showFirst) MyTextField(key: ValueKey('key0')),
            MyTextField(key: ValueKey('key1')),
            MyTextField(key: ValueKey('key2')),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState((){
            showFirst = false;
          });
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
```

### 利用シーン
* リスト内の項目で一意になる値がある場合
* 自分で一意の値を設定したい場合
* リストの項目が変化する場合

### 継承関係
Object > Key > LocalKey > ValueKey

## [ObjectKey](https://api.flutter.dev/flutter/widgets/ObjectKey-class.html)
`ObjectKey`はObject (WidgetやState等) を引数にして生成する Key です。例えば、ユーザ名やユーザID, 性別, 電話番号など、複数の情報を保有したリストを表示する場合において、各アイテムのWidgetを識別する時などに利用します。

### ValueKyeとの違い
`ValueKey`が1つの情報で特定できる Key であるのに対して、`ObjectKey`は複数の情報で特定できる Key という位置付けです。

### ソースコードを見てみる
`ObjectKey`のソースコードを見るとわかりやすいです。

ObjectKeyの引数には`Object`を指定し、何の情報でもKeyに指定できるようになっています。また、operator の部分で`runtimeType` (クラス名) と identical (インスタンスが同じかどうか比較) で Key の比較を実現していますね。これを見ると、`ObjectKey`の多用はパフォーマンス観点では良くないです。可能なら (主キーがあるなら) 、`ValueKey`の方が良いでしょう。

```Dart:packages/flutter/lib/src/widgets/framework.dart
class ObjectKey extends LocalKey {
  /// Creates a key that uses [identical] on [value] for its [operator==].
  const ObjectKey(this.value);

  /// The object whose identity is used by this key's [operator==].
  final Object value;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ObjectKey
        && identical(other.value, value);
  }

  @override
  int get hashCode => hashValues(runtimeType, identityHashCode(value));

  @override
  String toString() {
    if (runtimeType == ObjectKey)
      return '[${describeIdentity(value)}]';
    return '[${objectRuntimeType(this, 'ObjectKey')} ${describeIdentity(value)}]';
  }
}
```

### 継承関係
Object > Key > LocalKey > ObjectKey

## [PageStorageKey](https://api.flutter.dev/flutter/widgets/PageStorageKey-class.html)

`PageStorageKey`はよく出てくる話が、Tab + ListViewの構成時にタブ切り替えてもスクロール位置をキープするために利用するものという内容です。まさにこの通りですね。

仕組み的には、`PageStorageKey`は他のLocalKeyとは少し扱いが異なります。[ソースコード](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/page_storage.dart)を見ると何となく用途が分かってくると思います。Flutterはページが切り替わっても、各 Widget の State を保存できる様に、[`PageStorage`](https://api.flutter.dev/flutter/widgets/PageStorage-class.html)という機能 (クラス) を用意しています。これを利用することで、任意の情報をストアしたりロードしたり出来ます。

タブ操作でページが切り替わり、元のページに戻った時の表示を本来律儀に対応しようとすると、各 Widget に`Pagestorage`で保存されていた (Page切り替わり時に保存した) 状態の情報をロードしてあげて、スクロール位置を調整するという作業が必要になります。ただ、アプリ開発者がこれを毎回やりたくないですよね？

そこでもっと便利になるようにと用意されたのが、この`PageStorageKey`です。これを利用すれば、Key を指定するだけでページが切り替わり前後の状態を自動で対応してくれるため、大変便利です。

### 使い方

使い方自体は特別特殊なことはなく、`ValueKey`や`ObjectKey`と同じ様に利用します。

```Dart:
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
          bottom: TabBar(
            tabs: <Widget>[
              Tab(
                child: Text('Tab-1'),
              ),
              Tab(
                child: Text('Tab-2'),
              ),
            ],
          ),
        ),
        body: TabBarView(
          children: <Widget>[
            // Stateが保存されないパターン
            //_TabPage(tab: 0),
            //_TabPage(tab: 1),

            // PageStorageKeyを利用してStateを保存するパターン
            _TabPage(key: PageStorageKey(0), tab: 0),
            _TabPage(key: PageStorageKey(1), tab: 1),
          ],
        ),
      ),
    );
  }
}

class _TabPage extends StatefulWidget {
  _TabPage({Key key, this.tab}) : super(key: key);

  final int tab;

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

class _TabPageState extends State<_TabPage> {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 100,
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title:
            Text('${widget.tab}: Item $index'),
        );
      },
    );
  }
}
```

### 継承関係
Object > Key > LocalKey > ValueKey<T> > PageStorageKey

# その他カテゴリ

## [AssetBundleImageKey](https://api.flutter.dev/flutter/painting/AssetBundleImageKey-class.html)

その他にあまり使うシーンは無いと思いますが、Key と名が付くもので`AssetBundleImageKey`というものがあります。
[`AssetImage`](https://api.flutter.dev/flutter/painting/AssetImage-class.html)や[`ExactAssetImage`](https://api.flutter.dev/flutter/painting/ExactAssetImage-class.html)のアセット系を利用する場合のIDとして利用します。

これは特に覚える必要性もないでしょう。

### サンプルコード (の紹介)

https://github.com/fluttercandies/extended_image_library/blob/master/lib/src/extended_asset_bundle_image_provider.dart