[Flutter]ドラック & ドロップで自由に場所を変えられるウィジェット

意外に作るのが大変だったので共有。

# 完成品(作りたいもの)
こういうの
![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470374/4e719bde-ce7a-6df9-6b3e-d05e9fd8145e.gif)

# ドラック & ドロップできるウィジェット
当然ながら、Flutterではドラック & ドロップできるウィジェットが用意されている。


https://api.flutter.dev/flutter/widgets/Draggable-class.html

要約すると、以下のように比較的簡単にドラッグ対象とドロップ対象を実装できる。
ほかにもオプションがあるが、ここでは略。

```dd.dart
//ドラッグ対象
 Draggable(
    child: <ドラッグしたいウィジェット>,
    feedback: <ドラッグ中に表示するWidget>,
    data: <ドロップしたときにDropTargetに渡す情報>
)

//ドロップ対象
DropTarget(
    builder: (context,accepted,rejected){
        child: <ドロップ対象のウィジェット>
    },
    onAccepted:(data){<ドロップ時完了時処理>}
)

``` 

# 入れ替え処理の実装
まずはどういった処理が実装できればいいか整理する。

条件1. ある要素Aをドラッグして別の要素Bにドロップしたとき、Bの前or後に要素Aを追加し、元の要素Aは削除する。
![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470374/f343b72f-65ff-acba-b000-b80fe107e001.png)

条件2. ある要素Aをドラッグして、ドロップした先に要素が存在しない場合は、その列の最後尾に要素Aを追加し、元の要素Aは削除する。
 ![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470374/929c7f19-ee73-6939-4b98-d8b155d7e90f.png)

条件1を考えると、各要素はドラッグ可能であり、かつドロップ先としても有効である必要がある。
条件2を考えると、各要素のバック(黒い箱)もドロップ先として有効である必要がある。
つまり、実装はざっくりと、

```dd.dart
//黒い箱
DragTarget(
    child: ListView( //各要素のリストとして表示
        children:[
                <要素A>,
                <要素B>,
                ...
                <要素X>
            ]
    )
)

//要素A
要素A = DragTarget(
    builder: (context,accepted,rejected){
    return Draggable(
        ...
        )
    }
)

//要素B
...

```

こんな感じ。ややこしい。

# 各要素の状態管理
シンプルに多重リストで管理する。状態管理にはflutter hooksを用いる。
hooksて何？ていう人は以下を見てほしい。
StateFulWidgetと比較するとかなり記述が楽になる。

https://qiita.com/mkosuke/items/f88419d0f4d41ed6d858

```dd.dart
//初期設定
final nums = useState([
    [1,2,3,4,5],
    [6,7,8,9,10]
])
```
たとえば5を移動させたときに
```dd.dart
nums.value = [
    [1,2,3,4],
    [5,6,7,8,9,10]
]
```
こんな感じになるように実装する。

# コード全文
```dd.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData.dark(),
      darkTheme: ThemeData.dark(),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MyHomePage extends HookConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final nums = useState([
      [1, 2, 3, 4, 5],
      [6, 7, 8, 9, 10]
    ]);

    Widget drugContainer(int gid, int id, Color color) {
      return DragTarget(onAccept: (Map data) {
        int gid0 = data["gid"];
        int id0 = data["id"];
        List lnums = [...nums.value];

        if (gid == gid0 && id == id0) {
          // 元の場所に戻した時には何もしない
        } else {
          //元要素の削除
          lnums[gid0].removeAt(nums.value[gid0].indexOf(id0));

          //要素の追加
          lnums[gid].insert(nums.value[gid].indexOf(id), id0);
          nums.value = [...lnums];
        }
      }, builder: (context, accepted, rejected) {
        return Draggable(
            data: {"gid": gid, "id": id},
            feedback: Material(
                child: Container(
                    width: 100,
                    height: 100,
                    color: color,
                    child: Text(id.toString(),
                        style: const TextStyle(fontSize: 30)))),
            child: Container(
                width: 100,
                height: 100,
                color: color,
                child:
                    Text(id.toString(), style: const TextStyle(fontSize: 30))));
      });
    }

    void listreset() {
      nums.value = [
        [1, 2, 3, 4, 5],
        [6, 7, 8, 9, 10]
      ];
    }

    Widget movableListView(i, widgetList) {
      return DragTarget(onAccept: (Map data) {
        int gid0 = data["gid"];
        int id0 = data["id"];
        List lnums = [...nums.value];

        //元要素の削除
        lnums[gid0].removeAt(nums.value[gid0].indexOf(id0));

        //要素の追加
        lnums[i].add(id0);

        nums.value = [...lnums];
      }, builder: (context, accepted, rejected) {
        return Column(children: [
          const SizedBox(
            height: 25,
          ),
          Container(
            color: Colors.black,
            child: SizedBox(
                height: 100,
                child: ListView(scrollDirection: Axis.horizontal, children: [
                  for (int j in widgetList)
                    drugContainer(i, j, j.isEven ? Colors.blue : Colors.red)
                ])),
          ),
          const SizedBox(
            height: 25,
          )
        ]);
      });
    }

    return Scaffold(
        body: Center(
      child: Column(children: [
        ElevatedButton(
            onPressed: () {
              listreset();
            },
            child: const Text("reset")),
        const SizedBox(
          height: 100,
        ),
        for (int i = 0; i < nums.value.length; i++)
          movableListView(i, nums.value[i]),
        const SizedBox(
          height: 50,
        )
      ]),
    ));
  }
}

```

# 最後に
そもそももっと楽なプラグインあるよ、みたいなのがあれば教えてください。