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

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

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

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

# 要素を並び替えられる ListView
デフォルトで入っている`ReorderableListView`&`SliverReorderableList`を使います。

https://api.flutter.dev/flutter/material/ReorderableListView-class.html

## ReorderableListView の使い方
* 基本的には`ListView`と同じ使い方
* `itemBuilder`には並べたい要素を生成する関数を指定します。
* `ListView`と異なる点としては、各要素にユニークな`Key`を指定する必要があることです。
* `onReorder`には並び替えが完了したとき（ドラッグしている要素を放したとき）に行う処理を記載します。
* `proxyDecorator`は任意に指定可能で、ドラッグしている要素をデコレーションできます（今回のコードでは、ドラッグする要素を半透明にしています。

<details><summary>余談</summary>

VueやReactのリストでもkeyを指定する必要があり、keyには`index`は使わないほうがいいみたいなのをどこかで見た気がするが、Flutter公式サンプルが`index`を使っていたので使っても大丈夫そう。
</details>

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

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

    return Scaffold(
      appBar: AppBar(),
      body: ReorderableListView.builder(
        itemBuilder: (_, index) => ItemTile(
          items[index],
          key: Key('$index'), // 各要素にユニークなKeyをつける必要がある
        ),
        itemCount: items.length,
        onReorder: (int oldIndex, int newIndex) {
          _onReorder(items, oldIndex, newIndex);
        },
        proxyDecorator: (widget, _, __) {
          return Opacity(opacity: 0.5, child: widget);
        },
      ),
    );
  }

  void _onReorder(List<Item> items, int oldIndex, int newIndex) {
    if (oldIndex < newIndex) {
      newIndex -= 1;
    }
    items.insert(newIndex, items.removeAt(oldIndex));
  }
}
```
:::note
`ReorderableListView(children: [])`という書き方もできます。
:::

## SliverReorderableList の使い方
* `ReorderableListView`と異なり、並べたい要素を`ReorderableDragStartListener`または`ReorderableDelayedDragStartListener`でラップする必要があります。
* その影響で、ユニークなkeyを付与する対処も異なるので注意。

```dart
            itemBuilder: (_, index) => ReorderableDelayedDragStartListener(
              index: index,
              // ItemTileではなく、ReorderableDelayedDragStartListenerにkeyを付与
              key: Key('$index'), 
              child: ItemTile(items[index]),
            ),
```

* `ReorderableDragStartListener`と`ReorderableDelayedDragStartListener`の違いはドラッグ開始までのタップ時間。`ReorderableDragStartListener`はほぼタップした瞬間にドラッグが始まりますが、`ReorderableDelayedDragStartListener`は長押ししたらドラッグが始まります。
* `ReorderableDragStartListener`は画面全体のスクロールがしづらくなってしまうため、要素が多くスクロールが必要な場合は`ReorderableDelayedDragStartListener`を使うのがおすすめです。
* `ReorderableListView`と同様、`proxyDecorator`を指定することでドラッグ中の要素をデコレーションできます。


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

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

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          const SliverAppBar(),
          SliverReorderableList(
            // ReorderableListViewと異なり、
            // 各要素を ReorderableDragStartListener または ReorderableDelayedDragStartListener
            // でラップする必要がある。
            // その影響もあり、keyを付与する対象は ReorderableDragStartListener または
            // ReorderableDragStartListenerになるので注意。
            //
            // ReorderableDragStartListenerはタップで移動が開始されるが、
            // ReorderableDelayedDragStartListenerはロングタップで移動開始となる。
            // タップで要素の移動が始まってしまうと
            // スクロールがしづらくなるので ReorderableDelayedDragStartListener のほうがいい
            itemBuilder: (_, index) => ReorderableDelayedDragStartListener(
              index: index,
              key: Key('$index'),
              child: ItemTile(items[index]),
            ),
            itemCount: items.length,
            onReorder: (int oldIndex, int newIndex) {
              _onReorder(items, oldIndex, newIndex);
            },
            proxyDecorator: (widget, _, __) {
              return Opacity(opacity: 0.5, child: widget);
            },
          ),
        ],
      ),
    );
  }

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