Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

はじめに

Flutterで時々利用するWidget の Keyについてのまとめです。

Keyが必要な理由と仕組みについて (1回目)

主にKeyとは何か?、そもそもKeyが必要になる理由についてまとめています。
Flutter WidgetにKeyが必要な理由, 仕組みについて

Keyの種類一覧と使い方 (2回目) ← 今回!

Keyの種類一覧とそれらの使い方について解説します。

Keyの指定位置について (3回目)

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

Keyの種類について

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

利用するKeyのスコープに合わせて、GlobalKeyなのかLocalKeyなのかを決め、その後、用途に合わせて最終的に利用する派生クラスのKeyを選択します。
スクリーンショット 2020-04-04 14.13.39.png

GlobalKeyカテゴリ

GlobalKey

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

利用シーン

  • 2つの異なる画面で同じ状態のWidgetを表示したい場合
  • 他のWidgetから特定のWidgetを参照したい場合

継承関係

Object > Key > GlobalKey

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

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

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

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

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

@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 > GlobalObjectKey

使用例 (参考文献)

LabeledGlobalKey

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

@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を利用していますが、一般の開発者が利用するものではないと思いますので、覚えなくて良いでしょう。

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 > LabeledGlobalKey

LocalKeyカテゴリ

LocalKey

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

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

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つ用意し、一番上TextFieldをFABのクリックに連動して非表示にする (したい) コードです。

ダメな例
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が非表示になります (なった様に見えます)。
ezgif.com-video-to-gif.gif

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

正しい例
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();
}

意図した通り、動作する様になりました!
ezgif.com-video-to-gif.gif

参考文献

UniqueKey

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

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

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

使い方

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

使用例
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

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

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

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するのは無駄ではありますが…。

使用例
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

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

ValueKyeとの違い

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

ソースコードを見てみる

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

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

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

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

仕組み的には、PageStorageKeyは他のLocalKeyとは少し扱いが異なります。ソースコードを見ると何となく用途が分かってくると思います。Flutterはページが切り替わっても、各 Widget の State を保存できる様に、PageStorageという機能 (クラス) を用意しています。これを利用することで、任意の情報をストアしたりロードしたり出来ます。

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

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

使い方

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

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 > PageStorageKey

その他カテゴリ

AssetBundleImageKey

その他にあまり使うシーンは無いと思いますが、Key と名が付くものでAssetBundleImageKeyというものがあります。
AssetImageExactAssetImageのアセット系を利用する場合のIDとして利用します。

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

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

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

kurun_pan
QiitaではFlutterに関する記事を投稿しています。その他の技術内容やQiita投稿記事の内容以外についての、ご意見・連絡等はTwitterの方へお願いします! 
https://kurun.booth.pm/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away