【Flutter】各要素を並べ替えできる GridView を使ってみる

# 成果物
##### デモ
https://popy1017.github.io/flutter_reorder_list_sample/

:::note warn
Web版だと`ReorderableListView`のみ挙動が異なります。
(右側の並び替えアイコンを長押ししないと並び替えできない。)
:::

##### ソースコード
https://github.com/popy1017/flutter_reorder_list_sample

##### ListView
https://qiita.com/popy1017/items/8ff21a68678af65eb046

# 並べ替えできるGridView
以下のパッケージを使いました。
使い方も`ReorderableListView`とほとんど同じで、非公式とは思えないほど使いやすかったです。
Sliver対応の`SliverReorderableGrid`クラスもあります。

https://pub.dev/packages/reorderable_grid

## ReorderableGridView の使い方

* ほとんど`ReorderableListView`と同じ。
* `itemBuilder`に並べたい要素を返す関数を指定し、各要素にはユニークな`key`を指定します。
* コードにはないですが、こちらも`proxyDecorator`を指定することでドラッグ中の要素の見た目を変えることができます。
* `gridDelegate`には`GridView`でおなじみの`SliverGridDelegateWithFixedCrossAxisCount`を指定。（他にもあった気がします）
* `ReorderableListView`と同じく、`.builder`を使わない書き方もOK。

```dart
class ReorderableGridViewSample extends ConsumerWidget {
  const ReorderableGridViewSample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(itemsProvider);

    return Scaffold(
      appBar: AppBar(),
      body: ReorderableGridView.builder(
        itemBuilder: (_, index) => ItemCard(
          items[index],
          key: Key('$index'), // 各要素にユニークなKeyをつける必要がある
        ),
        itemCount: items.length,
        onReorder: (int oldIndex, int newIndex) =>
            _onReorder(items, oldIndex, newIndex),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: kIsWeb ? 4 : 2,
        ),
      ),
    );
  }

  void _onReorder(List<Item> items, int oldIndex, int newIndex) {
    final item = items.removeAt(oldIndex);
    items.insert(newIndex, item);
  }
}
```

## SliverReorderableGrid の使い方
* こちらもほぼ `SliverReorderableList` と同じ。すごい。
* ドラッグリスナーとしては`ReorderableGridDragStartListener`または `ReorderableGridDelayedDragStartListener`が用意されており、各要素をいずれかでラップする。
* ユニークな`key`は並べたい要素自体（ここでは`ItemCard`）ではなく上記のいずれかのリスナーに付ける必要があるので注意。

```dart
class SliverReorderableGridPage extends ConsumerWidget {
  const SliverReorderableGridPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(itemsProvider);

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          const SliverAppBar(),
          SliverReorderableGrid(
            // SliverReorderableListと同様、こちらも各要素を
            // ReorderableGridDragStartListener または ReorderableGridDelayedDragStartListenerで
            // ラップする必要がある。
            itemBuilder: (_, index) => ReorderableGridDelayedDragStartListener(
              index: index,
              key: Key('$index'),
              child: ItemCard(
                items[index],
                key: Key('$index'), // 各要素にユニークなKeyをつける必要がある
              ),
            ),
            itemCount: items.length,
            onReorder: (int oldIndex, int newIndex) =>
                _onReorder(items, oldIndex, newIndex),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: kIsWeb ? 4 : 2,
            ),
            proxyDecorator: (widget, _, __) {
              return Opacity(opacity: 0.5, child: widget);
            },
          ),
        ],
      ),
    );
  }

  void _onReorder(List<Item> items, int oldIndex, int newIndex) {
    final item = items.removeAt(oldIndex);
    items.insert(newIndex, item);
  }
}
```

# 不具合？
* そんなに大したことではありませんが、ドラッグ中に元の位置に戻せないです(`v1.0.3`時点)。