実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

順序変更可能なリスト表示・ReorderableListView

はじめに

本記事ではリスト表示の並び替えについてポイントを述べていきます。

まずはシンプルに順序変更可能なリストビューの基本的な使い方について見ていきます。殆どの部分でいつものListViewと使い方に変わりがない事がわかると思います。

ここではオフィシャル文書のサンプルコードを少しアレンジして、ReorderableListView.builder()の名前付きコンストラクタを利用していきます。ListView同様、builderのコンストラクタの方が扱いやすい場面が多いですからね。





順序変更可能リスト

スクリーンショット

まずはスクリーンショットで動きを見ていきます。

今回のサンプルコードでは50個の項目を並べています:

f:id:linkedsort:20211115201609p:plain:w400


これはブラウザで動作させたときのデフォルトのUIなのですが、右の二重線のところをつまむと項目がつまめます。いま下の画面では「項目1」をつまんでいます:

f:id:linkedsort:20211115201749p:plain:w400


つまんだ項目を自由な場所にドロップできます。ここでは項目2の下においてみます。順序が入れ替わりましたね。これを自由に繰り返すことができます:

f:id:linkedsort:20211115201846p:plain:w400


ところでこの並べ替えのUIは処理系によって変わるので注意が必要です。

例えばAndroidで実行した例が下記です。先程表示されていた右の二重線がなく、つまむ場所がみあたりません:

f:id:linkedsort:20211115202837p:plain:w400


Androidではリストビューの並び替えはロングタップがデフォルトの操作になっています。ですので、ぐーっと2秒位つまんでいると、項目が浮き上がります(項目2のところが浮かんでるのが見えると思います):

f:id:linkedsort:20211115202920p:plain:w400


サンプルコード

以下にサンプルコードを示していきます。今回は「lib/main.dart」の1ファイルのみです:

import 'package:flutter/material.dart';

//(1) リストに表示するデータクラス
class ListItem {
  //タイトル
  String title;

  //項目番号
  int number;

  ListItem(this.title,this.number);
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        home: Scaffold(
          appBar: AppBar(title: const Text("順序可変リスト")),
          body: Home(),
        ));
  }
}

class Home extends StatelessWidget {
  Home({Key? key}) : super(key: key);
  //(2) 初期リストの定義
  var list = List<ListItem>.generate(50, (int i) => ListItem("項目$i",i));

  @override
  Widget build(BuildContext context) {
    //(3) 順序変更可能のListView
    return ReorderableListView.builder(
        itemCount: list.length,
        itemBuilder: (context, i) =>
            makeItemCard(list[i],context),
        onReorder: (int oldIndex, int newIndex) {
          if (oldIndex < newIndex) {
            newIndex -= 1;
          }
          final ListItem item = list.removeAt(oldIndex);
          list.insert(newIndex, item);
        }
    );
  }

  //リストビューに表示するCard作成
  Widget makeItemCard(ListItem item, BuildContext context) {
    //(4) 公式サンプルのカラーをそのまま持ってきています
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
    final Color evenItemColor = colorScheme.primary.withOpacity(0.15);

    //(5) Card作成
    return Card(
      key: Key(item.number.toString()),
      child: ListTile(
        leading: const Icon(Icons.wb_sunny),
        title: Text(item.title),
        tileColor: item.number.isOdd ? oddItemColor : evenItemColor,
      ),
    );
  }
}

コードのポイント

コメントに番号を付した点について、説明していきます。

(1) リストの項目のクラス
//(1) リストに表示するデータクラス
class ListItem {
  //タイトル
  String title;

  //項目番号
  int number;

  ListItem(this.title,this.number);
}

ここでは文字列と項目のID番号の数字をもつクラスを定義しています。numberは後に重複していないことを前提としたKeyのIDとして使います。

(2) サンプルデータ生成
  //(2) 初期リストの定義
  var list = List<ListItem>.generate(50, (int i) => ListItem("項目$i",i));

リストビューに表示するリストの初期化を行っています。通常のアプリでは何らかのデータのリストがすでにあることが想定されますが、ここではサンプルですのでループを使って50個の要素を生成しています。

(3) 順序変更可能なListViewの設定
  //(3) 順序変更可能ListView
  return ReorderableListView.builder(
      itemCount: list.length,
      itemBuilder: (context, i) =>
          makeItemCard(list[i],context),
      onReorder: (int oldIndex, int newIndex) {
        if (oldIndex < newIndex) {
          newIndex -= 1;
        }
        final ListItem item = list.removeAt(oldIndex);
        list.insert(newIndex, item);
      }
  );

ここが今回のメインポイント、順序変更可能なListViewです。

基本的にいつものListView.builderと同じ形ですが、クラス名がReorderableListViewになっている部分が違いその1

そして順序を変更しようとする操作があるときに呼び出される「onReorder」に設定するコールバック関数が、違いその2です。

コールバック関数の引数oldIndexは、つまみ上げられた項目のもともとのインデックス番号、newIndexがつまんだ項目をドロップした場所のインデックス番号です。

この情報を使ってリストを更新する処理を行います。基本は古いインデックスにあるitemをremoveして(その返り値が取り除かれた項目)、これを新しいインデックスの場所に挿入しています。これは動作の通り、直感的な操作ですよね。ただ注意点としてonReorderの冒頭にありますが、oldIndexがnewIndexより前にある場合は、oldIndexが取り除かれるのでnewIndexの一つ前にずれるという調整が入ります。

この操作はオフィシャルのReorderableListViewクラスのドキュメントでもサンプルとしてあげられているそのままの定番処理で、同じ考え方をそのまま適用する形でOKです。

(4) 項目の色分け
  //(4) 公式サンプルのカラーをそのまま持ってきています
  final ColorScheme colorScheme = Theme.of(context).colorScheme;
  final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
  final Color evenItemColor = colorScheme.primary.withOpacity(0.15);

少し複雑な表現になっていますが、これもオフィシャルのドキュメントのサンプルをそのまま持ってきて見た目を合わせています。

特にReorderableListViewと関係があるわけではありません。この交互の色の付け方と順序変更は少し相性がよくないかもしれませんね。

(5) 順序変更の対象となる要素のウィジェット
  //(5) Card作成
  return Card(
    key: Key(item.number.toString()), 
    child: ListTile(
      leading: const Icon(Icons.wb_sunny),
      title: Text(item.title),
      tileColor: item.number.isOdd ? oddItemColor : evenItemColor,
    ),
  );

いつものListViewの要素と同様CardでくるんだListTileウィジェットの構成にしています。

ここでいつもと違う点がまず「key」の指定が必須なところです。ここで指定しているKeyクラスはウィジェットのIDとなるものでReorderableListViewはその要素にユニークなKeyの指定を要求しています。なのでここでは同一の値にならないように引数を設定しつつKeyクラスのコンストラクタを呼び出しています。

item.numberはそれぞれの要素で異なる数値になっていますので、重なる心配はありません。

一点注意はkeyパラメタをListTileの方で設定してしまうことです。ListViewという意識があるのでうっかりやりがちですが、ReorderableListViewが直接見えているのはCardウィジェットです。

ListViewのtileColorは要素の数字の偶数・奇数で色を分けています。





おわりに

今回は順序変更可能なリストビューについて、シンプルな例でポイントをまとめました。

onReorderでの順序の入れ替え以外はほとんどListViewと変わりませんし、onReorderも定番処理を実行すればよいだけですので、使い方は単純ですよね。

これが実際のアプリになってくると、順序変更を永続化してアプリが中断されても変更された順序を継続する必要があります。このあたりを反映した実際のアプリに近い文脈での活用の仕方を次回、示していきたいと思います。

f:id:linkedsort:20211116003532j:plain