【Flutter】主なKeyと使い分け

  • 2024年5月14日
  • 2024年5月14日
  • Widget

はじめに

Flutterの異なるキーにはそれぞれ具体的な機能があり、Widgetの状態や識別の管理に役立ちます。ここではGlobalKey、LocalKey、ValueKey、UniqueKey、そしてObjectKeyの使用時の目的と適用場面を解説します。

Keyを使う目的

通常、Flutterのフレームワークはウィジェットを識別する際に、そのruntimeType(ウィジェットの型)とリストやツリー内での順序に基づいてマッチングを行います。しかし、この方法だけでは、リストのアイテムが移動したり、追加や削除が行われたりした場合に正確な識別が難しくなることがあります。ここでKeyの役割が重要になります。
Keyをウィジェットに割り当てることで、フレームワークにウィジェットが持つ一意性を伝えることができます。これにより、同じruntimeTypeを持つウィジェットであっても、Keyが異なれば異なるウィジェットとして扱われます。また、Keyが同じ場合は同一のウィジェットとみなされ、状態の保持が可能となります。
リストにおけるウィジェットの追加、削除、並び替えは、Keyを使用することでスムーズに行えます。たとえば、リストからアイテムを削除または追加する際、Keyを使うことで各アイテムの状態を正確にフレームワークに伝えることができます。これにより、アイテムの順番が変わっても、各アイテムの状態(例えばスクロール位置や入力されたテキストなど)が保持されます。

GlobalKey

目的: Widgetツリー全体でWidgetへのグローバルアクセスを提供し、複数のWidget間での状態管理や複雑なWidgetの相互作用に最適です。
使用例: Widgetの状態をそのツリーの外からアクセスしたり、動的なWidget更新中に状態を維持する場合に役立ちます。

GlobalKey<CounterWidgetState> _counterKey = GlobalKey<CounterWidgetState>();

FloatingActionButton(
  onPressed: () => _counterKey.currentState?.incrementCounter(),
  child: Icon(Icons.add),
);

LocalKey

LocalKeyは特定のWidgetツリーの一部でのみ一意性を保証しますが、直接使用されることはありません。ValueKeyやUniqueKeyなどの具体的なKeyの基底クラスです。

UniqueKey

目的: フレームワークにWidgetを完全に新しいものとして扱わせ、Widgetの状態の保存を防ぎます。
使用例: アイテムが順序だけでなく内容も変更される動的リストで、Widgetを完全に再構築したい場合に有用です。

ListView.builder(
  itemBuilder: (context, index) => ListTile(
    key: UniqueKey(),
    title: Text('Item $index'),
  ),
);

ValueKey

目的: 値の等価性に基づいてWidgetを識別します。ValueKeyの内部の値が等しい場合、Widgetは等価と見なされます。
使用例: リストやコレクションでアイテムの状態を更新にもかかわらず維持する必要がある場合に最適です。

ListView.builder(
  itemBuilder: (context, index) => ListTile(
    key: ValueKey(items[index].id),
    title: Text(items[index].title),
  ),
);

ObjectKey

目的: オブジェクトの同一性を基にWidgetを識別します。特定のオブジェクトを基にWidgetが同一であるかをフレームワークに伝えるため、複数のWidgetが同じ型であっても、異なるオブジェクトを持っていればそれぞれ別々のWidgetとして扱われます。
使用例: オブジェクトの特性を基にしたWidgetの一意性を保証したい場合に適しています。例えば、ユーザーごとに異なるプロファイルWidgetを管理する場合などに使用されます。

ListView.builder(
  itemBuilder: (context, index) => ListTile(
    key: ObjectKey(users[index]),
    title: Text(users[index].name),
  ),
);

ObjectKeyとオブジェクト同一性:

ObjectKeyはオブジェクトの同一性に基づいてウィジェットを識別するために使用されます。これは、オブジェクトが持つ内容(プロパティの値など)ではなく、オブジェクト自体の参照に基づいて識別することを意味します。そのため、内容が同じでも異なるインスタンス(メモリ上の異なる場所に存在するオブジェクト)であれば、異なるキーとして扱われます。

この特性は、ウィジェットの一意性を保証するために重要ですが、開発者が期待する動作と異なる場合があります。例えば、次のテストケースを見てみましょう。

group('ObjectKey', () {
  test('同一のオブジェクトでないと不一致', () {
    final person1 = Person('name', 20);
    final person2 = Person('name', 20);
    expect(person1, person2);

    expect(ObjectKey(person1), isNot(ObjectKey(person2)));
  });

  test('同一のオブジェクトであれば、変更されても一致', () {
    var person = Person('name', 20);
    final key1 = ObjectKey(person);
    final key2 = ObjectKey(person);
    expect(key1, key2);

    person.name = 'name2';
    final key3 = ObjectKey(person);
    expect(key1, key3);
  });
});

ここでの重要な点は、person1person2が内容は同じでも異なるオブジェクトであるため、ObjectKey(person1)ObjectKey(person2)は異なると評価されることです。一方で、personのプロパティを変更しても、それは同一のオブジェクトの参照を持っているため、ObjectKeyは引き続き同一と評価されます。

この挙動は、特に動的な内容を持つリストやコレクションを扱う場合に影響を与える可能性があります。新しいオブジェクトを生成するたびに新しいキーが割り当てられるため、ウィジェットの状態やアニメーションが意図しない方法でリセットされることがあります。このような挙動を理解し、適切に対応することが、Flutterで効果的なUIを構築する鍵です。

まとめ

ということで、Keyをまとめてみました。
ObjectKeyの挙動がちょっと意外でした(プロパティが一緒であれば、同一になると思ってた)。

参考

ソース(コピペしてテストで動作確認用)

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class Person {
  Person(this.name, this.age);

  String name;
  int age;

  @override
  int get hashCode => name.hashCode + age.hashCode;

  @override
  bool operator ==(Object other) {
    if (runtimeType != other.runtimeType) {
      return false;
    } else {
      final another = other as Person;
      return name == another.name && age == another.age;
    }
  }
}

void main() {
  test('UniqueKey', () {
    final key1 = UniqueKey();
    final key2 = UniqueKey();

    expect(key1, isNot(key2));
  });

  test('ValueKey', () {
    const key1 = ValueKey('value1');
    const key1_ = ValueKey('value1');
    const key2 = ValueKey('value2');

    expect(key1, key1_);
    expect(key1, isNot(key2));
  });

  group('ObjectKey', () {
    test('同一のオブジェクトでないと不一致', () {
      final person1 = Person('name', 20);
      final person2 = Person('name', 20);
      expect(person1, person2);

      expect(ObjectKey(person1), isNot(ObjectKey(person2)));
    });

    test('同一のオブジェクトであれば、変更されても一致', () {
      var person = Person('name', 20);
      final key1 = ObjectKey(person);
      final key2 = ObjectKey(person);
      expect(key1, key2);

      person.name = 'name2';
      final key3 = ObjectKey(person);
      expect(key1, key3);
    });
  });
}