【Flutter】ListViewでD&Dソートに対応しつつ並び順を保存する


はじめに

Flutterだとドラッグアンドドロップで並べ替え可能なListView自体はかなり楽に実装できるのですが、ネイティブ同様(?)並び順の保存に関しては自前で作る必要があります(たぶん)。
もし適当に保存しようものなら並べ替えやアイテムの追加・削除でごちゃごちゃになってしまいます(経験済み)。
なんとなく一番簡単そうな方法を書き綴っておきますので良ければ参考にしてみてください(もっといい方法があればコメント頂けると嬉しいです)。
因みにFlutterは始めたばかりなのでおかしなところがあるかも知れません。

それとListView自体の実装とSQL関連の詳細説明は省略しますので適宜調べて貰えると幸いです。

前提

ListViewに並べるアイテムはDBに実装済みのものとします。
今回は以下のアイテムを用意して進めます。

item.dart
class Item {
  var id;
  var title;
  var sort;
  //その他省略
}

実装

ListViewの実装

先ずはD&Dソート可能なListViewを実装します。

Main.dart
class _MainPageState extends State<MainPageState> {

  List<Item> itemList = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text("Sample"),),
        body: FutureBuilder(
          future: _createList(),
          builder: (BuildContext context, AsyncSnapshot<List<Item>> snapshot){
            if (!snapshot.hasData) {
              return Text("Loading...");
            }else{
              return ReorderableListView.builder(//ReorderableListViewにすることでD&Dソート可能
                padding: EdgeInsets.all(10),
                itemCount: itemList.length,
                onReorder: (oldIndex, newIndex){//onReorderを追加、D&Dすると呼ばれる
                  if (oldIndex < newIndex) {
                    newIndex -= 1;
                  }
                  Item movedItem = itemList.removeAt(oldIndex);//アイテムの場所を入れ替える
                  itemList.insert(newIndex, movedItem);
                },
                itemBuilder: (context, index){
                  return _buildListItem(itemList[index], index);
                },
              );
            }
          },
        ),
    );
  }

  Widget _buildListItem(Item item, int index){
    return Container(
      key: Key(item.id),//ソートに必要な一意のキー(なんでもいい)
      child: Card(
        child: ListTile(
          onTap: (){},
          //onLongPress: (){},//無効化する
          title: Text(item.title),
          trailing: Text("$index/${item.sort}"),//インデックスと保存してある並び順
        ),
      ),
    );
  }

  Future<List<Item>> _createList() async {
    itemList = await DBProvider.db.getAllItem();//DBからとってくる
    itemList.sort((a,b) => a.sort.compareTo(b.sort));//保存済みの順番でソート
    return itemList;
  }

}

これでドラッグアンドドロップによるソートが可能なListViewが実装できました。
ポイントはListView.builderからReorderableListView.builderに変更すること、onReorderを追加すること、ListViewに並べるWidgetに一意のキーを持たせること、です。
ListViewに並べるWidgetの中身は特に何でもいいんですが分かりやすくする為に保存されている並び順とインデックスを表示してます。
次は並び順の保存を実装します。

並び順の保存

保存するタイミングは色々あると思いますが、今回はD&D時に毎回保存しようと思います。
先程のonReorder内に以下のメソッドを追加します。

main.dart
class _MainPageState extends State<MainPageState> {
   //省略

   onReorder: (oldIndex, newIndex){
   //省略
   _saveSort();
   }

   _saveSort(){
    itemList.asMap().forEach((index, item) {//asMap()でindexを持たせる
      item.sort = index.toString();//アイテムに保存する並び順を現在のインデックスに変える
      DBProvider.db.updateItem(item);//DBのアイテムを更新
    });
  }

}

これで_saveSort()が呼ばれるたびにD&D後のインデックスがDBに保存されると思います。
アイテムを追加したり削除したりしても並べ替え後のインデックスで上書きするので問題ないはずです(たぶん)。

ただ、これだと一つ問題というかUPDATE文が何回も呼ばれてしまうので、必要であればDBProviderを以下のように少し変更します。

db_provider.dart
class DBProvider {
   //省略

  updateItem(Item item) async {//こっちではなく
    final db = await database;
    var res = await db.update(
      tableName,
      item.toMap(),
      where: "id = ?",
      whereArgs: [item.id],
    );
    return res;
  }
  updateAllItem(List<Item> itemList) async {//こっちを追加
    final db = await database;
    var batch = db.batch();
    itemList.forEach((item) {
      batch.update(
        tableName,
        item.toMap(),
        where: "id = ?",
        whereArgs: [item.id],
      );
    });
    var res = await batch.commit();
    return res;
  }

}

main.dartも書き換えます。

main.dart
  _saveSort(){
    itemList.asMap().forEach((index, item) {//asMap()でindexを持たせる
      item.sort = index.toString();//アイテムに保存する並び順を現在のインデックスに変える
      //DBProvider.db.updateItem(item);//これを削除して
    });
    DBProvider.db.updateAllItem(itemList);//これを追加
  }


SQLあまり詳しくないのですがバッチ更新を行うことでパフォーマンスが良くなるみたいなので、データが多くなる場合はこのほうがいいっぽいです。

おしまい

問題があったり、もっといい方法があれば教えてください!