SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Flutterで始めるモバイルアプリ開発

LocalKeyがわかるとFlutterの理解がより深まる

Flutterで始めるモバイルアプリ開発 第26回

  • X ポスト
  • このエントリーをはてなブックマークに追加

FlutterのWidget管理の実装上で生じる問題とは

 LocalKeyを理解するためには、LocalKeyを指定していない場合についてさらにもう一歩踏み込んで考えてみると、実はわかりやすくなります。そこで最初にFlutterでLocalKeyを意識する必要ないケースの実装を改めて説明します。続いて、LocalKeyが必要なケースの実装について考えるという手順で説明します。

 どちらのケースでも図2のような簡単なリストの表示順を変更するコード例を用いて説明します。

図2:リスト表示の順番を変更するサンプルアプリのイメージ
図2:リスト表示の順番を変更するサンプルアプリのイメージ

LocalKeyを意識する必要がない実装例

 これまでの連載の中でも簡単なリスト表示をする例はいくつか紹介してきましたが、改めてListViewを使った場合の実装例をリスト1に示します。

[リスト1]ListViewを使ったリスト表示のコード例(lib/child/ListViewPage.dartの一部抜粋)
class _ListViewPage extends State<ListViewPage>{

  // (1) noとtitleだけをプロパティに持つTodoItemクラスのデータリスト
  List<TodoItem> _items = [];
  bool _sort = false;

  @override
  void initState() {
    super.initState();
    // (2) サンプルとなる実データを作成
    _items.add(TodoItem(0,"タスク - 1"));
    // : (省略)
    _items.add(TodoItem(5,"タスク - 6"));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('タスク一覧'),
        actions : [
          IconButton(
          onPressed: () {
            setState(() {
              //  (3) _itemsの順番を入れ替える
              _sort =! _sort;
              _items = List.from(_items.reversed);
            });
          },
          icon: _sort? const Icon(Icons.arrow_circle_up) : const Icon(Icons.arrow_circle_down))
        ]
      ),
      body: ListView.builder(
          // (4) リスト表示部分の実装
          itemCount: _items.length,
          itemBuilder: (context,index){
            return Container(
              // : (省略)
              margin: const EdgeInsets.all(5),
              height: 50,
              child: Center(
                child: Text(_items[index].title),
              ),
            );
          },
      ),
    );
  }
}

 (1)は単純なnoとtitleだけを持つTodoItemクラスというオブジェクトを持つリストを用意します。そして、サンプルとして表示するためのデータを(2)で作成します。

 実際にはこのデータを(4)で各Containerウィジェットとして表示します。それらのデータの順番を逆転させるための実装が(3)です。

 このようにLocalKeyの存在はここではまったく現れません。つまり、データと表示ウィジェットの関係、そしてそれらの表示位置などが画面作成時に固定されている場合には通常、LocalKeyは必要ありません。

 少々わかりにくく、歯切れの悪い表現ですが、StatelessWidgetで構成される表示の場合にはLocalKeyは必要ありませんと言えば少々分かりやすくなると思います。つまり、反対に言えば、StatefulWidgetでは問題になるケースがあります。

LocalKeyがないと問題となるケース

 続いて、LocalKeyがないと問題となる実装コードをこれから紹介します。ここで紹介するコードでは、ソート機能が正しく動かない問題が生じるようになっています。

 ただし、これらのコードは意図的に問題が分かりやすく生じるように再現したコードのため、少々、不自然な実装と感じるところがありますがその点はご了承ください。

 まず、先ほどのListViewを使ったコードを問題が生じやすいように書き換えたコードがリスト2です。ただし、「生じやすい」と記述しているのは、このコード自体で問題が生じるわけではないためです。

[リスト2]問題が生じやすいコードに書き換えた例(lib/child/LocalKeyPage.dartの一部抜粋)
class _LocalKeyPage extends State<LocalKeyPage>{

  // (1) Widgetのリスト
  List<Widget> _items = [];
  bool _sort = false;

  @override
  void initState() {
    super.initState();
    // (2) 表示する部品を作成
    _items.add(createTodoItemWidget(0,"タスク - 1"));
    // : (省略)
    _items.add(createTodoItemWidget(5,"タスク - 6"));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text('ローカルキーサンプル'),
          actions : [
            IconButton(
                onPressed: (){
                  //  _itemsの順番を入れ替える
                  setState(() {
                    _sort =! _sort;
                    _items = List.from(_items.reversed);
                  });
                },
                icon: _sort? const Icon(Icons.arrow_circle_up) : const Icon(Icons.arrow_circle_down))
          ]
      ),
      body: Column(
        children: _items,
      ),
    );
  }

  Widget createTodoItemWidget(int no,String title){
    // (3) Statefulのウィジェットを作成
    return TodoItemWidget(no, title);
  }
}

 先ほどのコードと違うのは、TodoItemという単純なデータ構造クラスのリストだったのに対して、今回のコードでは(1)のようにWidgetのリストになっています。そして、(2)のinitStateでそれぞれのリスト表示の各アイテムのためのWidgetを作成してしまいます。通常、画面を構成する部品はbuildメソッド内で構築しますが、ここが意図的に問題を生じやすくしているところです。

 また、作成するウィジェットは(3)で作成します。問題となる直接の原因はこのTodoItemWidgetの実装にあり、そのコードはリスト3のようになります。

[リスト3]問題が生じるコード例(lib/child/TodoItemWidget.dartの一部抜粋)
class _TodoItemWidget extends State<TodoItemWidget>{

  late int no;
  late String title;

  @override
  void initState() {
    super.initState();
    // (1) 引数をstateで管理するためのプロパティとして更新
    setState(() {
      no = widget.no;
      title = widget.title;
    });
  }

  @override
  Widget build(BuildContext context) {
    var child = Container(
      child: Center(
        // (2) stateで管理しているtitleを利用
        child: Text(title),
      ),
    );
    return child;
  }
}

 (1)では、TodoItemWidgetのコンストラクタで渡されたパラメータをState管理する変数として設定しています。このような実装も通常は行わないですが、ここでは意図的に問題が生じるようにしています。そして、(2)ではそのtitleプロパティの値を使ってテキスト表示します。

 このコードを実行しても、ソートがされずに表示されているアイテムの順番はかわりません。しかし、前述したコードと違いどこに問題があるのかわからない方も多いはずです。

LocalKeyを使ってただしく動くコードに変更する

 問題あるコードをLocalKeyを使ってただしく動くように変更するには、リスト4のようにStatefulWidgetのコンストラクタにキーを追加すると正しく動作するようになります。

[リスト4]問題が生じやすいコードに書き換えた例(lib/child/LocalKeyPage.dartの一部抜粋)
// : (省略)
class _LocalKeyPage extends State<LocalKeyPage>{
    Widget createTodoItemWidget(int no,String title){
        return TodoItemWidget(no, title,
            // キーを追加
            key: ValueKey(no)
        );
    }
}
// : (省略)

 Flutterではできるだけ必要のない画面更新はしないように処理の効率化をしています。しかし、今回の実装ではその効率化のための実装が問題を生じさせています。

 Flutterでは、同じ画面定義、つまり同じWidgetクラスであれば表示済みのものをできるだけ再利用しようとします。つまり、インスタンスが異なっていても同じWidgetであれば再利用可能であるかもしれないという判断をします。

 そこで、StatefulWidgetの場合、この再利用できるかどうかの識別が正しくできないためにこのような問題が生じています。ただし、正しくできないという意味は不具合があるわけではなく、処理の最適化や複雑な問題が生じないためにもプログラマ側に画面更新する範囲の指定を委ねたという意味になります。

 また、例ではValueKeyを使ってキーを指定しましたが、LocalKeyには表1のようなキーがあります。

表1:LocalKeyの主な種類
入力種類 説明
ValueKey ユニークとなる数値や文字列などの値を指定してキーを作成する
ObjectKey オブジェクトを利用して一意なキーを作成する
UniqueKey ランダムな一意のキーを作成する

 自分の実装に合わせて、一意なキーになるように指定すればいずれのクラスであっても問題ありません。

実際にLocalKeyが必要になるシーン例

 ここまでの説明では、問題が生じるコードをあえて書き、そして多少強引にLocalKeyが必要として説明しました。

 これは内部の理解とその原因がよりわかりやすくするためのことですが、実は図3のようなケースでも問題が生じます。そして、同じ実装でもFlutter内部では行っている意味は全く異なります。

図3:Widgetの表示更新のサイクル
図3:Widgetの表示更新のサイクル

 ソートのような全体の並び順の変更には、データ側で対応することのほうがより一般的と思いますが、利用者がアイテムの順番をドラッグ&ドロップで入れ替えたい場合にはデータ側の操作ではなくUI側、つまり。部品の再配置だけになってしまうはずです。

 そして、最初に示したListViewを使ったコードをそのまま再利用し、ドラッグ&ドロップで順番を入れ替えることを可能にしたコード例がリスト5になります。

[リスト5]ドラッグ&ドロップができるListViewのコード例(lib/child/DraggableListViewPage.dartの一部抜粋)
class _DraggableListViewPage extends State<DraggableListViewPage>{

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // : (省略)
      ),
      // (1) 順番の入れ替え可能なListView
      body: ReorderableListView.builder(
          itemCount: _items.length,
          itemBuilder: (context,index){
            return Container(
              // (2) キーを必要とする
              key: UniqueKey(),
            );
          }, onReorder: (int oldIndex, int newIndex){
            // (3) 順番を入れ替える処理
            setState(() {
              final TodoItem item = _items.removeAt(oldIndex);
              _items.insert(newIndex,item);
            });
          },
      ),
    );
  }
}

 Flutterではドラッグ&ドロップで順番の入れ替えが可能なListViewの実装がすでに存在し、(1)のようにListViewであった部分をReorderableListViewに書き換えるだけで済みます。

 ただし、(2)のように作成する子ウィジェットにはkeyが必ず必要になります。ここまでのLocalKeyの説明について理解していれば、keyが必須になった理由も分かると思います。また、順番の入れ替えを確定して実際に画面部品の再配置をする処理が(3)になります。

次のページ
便利な機能を実装するためのLocalKeyの使い方

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
Flutterで始めるモバイルアプリ開発連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 小林 昌弘(コバヤシ マサヒロ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/19106 2024/03/13 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング