4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Flutterのkeyの整理

Last updated at Posted at 2021-04-18

:book: Flutterの記事を整理し本にしました :book:

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

はじめに

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

まとめ

本チャプターは、下記のGoogleDeveloperDaysの情報をベースとしています。

Key

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

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

Elementについて先に理解しておきたい方は、先に開発の上級1:3つのツリー(Widget/Element/RenderObject)をご参照ください

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

pic1.png

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

pic2.png

keyの内部での使われ方

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

Screenshot_1618666637.png

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

pic3.png

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

変更されたかは以下のような手順で判断します。

  1. 同じタイプか比較し、異なっていれば更新する
  2. 同じkeyか比較し、異なっていれば更新する
    ※ただし、2において、keyは設定しない場合は、nullになっています。
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

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

ソースコードでの確認

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),
      ),
    );
  }
}
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

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

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

pic5.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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?