[Flutter] drag_select_grid_viewでドラッグ&タップで複数選択できるGridViewを作るときのポイント

# 今回作成したもの
[drag_select_grid_view](https://pub.dev/packages/drag_select_grid_view)パッケージを用いて、以下のようなUIを作成。
![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526994/0e838a67-5b9f-b116-0fe6-a15c2574464a.gif)

下記ソースコードは、本家のサンプルコードを改造したもの。
https://github.com/popy1017/drag_select_grid_view

# 執筆時点でのバージョン

```
drag_select_grid_view: ^0.3.1
```

# 基本的な使い方
パッケージの説明文にも記載されているが、基本的には`GridView.builder`と同じように使うことができるため使いやすい。

```dart
    return Scaffold(
      appBar: SelectionAppBar(
        selection: controller.value,
      ),
      body: DragSelectGridView(
        gridController: controller,
        itemCount: 20,
        itemBuilder: (context, index, selected) {
          return SelectableItem(
            index: index,
            selected: selected,
          );
        },
        gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 80,
        ),
      ),
    );
```
## `GridView.builder`と異なる点
### `itemBuilder`
`itemBuilder`が若干異なり、`BuildContext`,`int`だけでなく、選択状態を表す`bool`も渡す必要がある。サンプルコードでは`SelectableItem`に`bool selected`を渡すことで、選択中の要素とそうでない要素の見た目を変えている。

```dart:drag_select_grid_view.dart
/// Function signature for creating widgets based on the index and whether
/// it is selected or not.
typedef SelectableWidgetBuilder = Widget Function(
  BuildContext context,
  int index,
  bool selected,
);
```
### `DragSelectGridViewController`
`ScrollController`の他に、独自のコントローラー`DragSelectGridViewController`を引数に指定できる。pub.devの紹介ページの「Advanced usage」には「`DragSelectGridViewController.value`を使うことで選択する要素を直接指定できる」とあり、後述するポイントでも使用する。

### その他
その他の追加のパラメータとしては下記の２つ。

* `autoScrollHotspotHeight`: オートスクロールが発生する高さを指定できる。この値が大きいと画面の上端・下端から遠くてもオートスクロールが発生するようになる。デフォルトは`64`。
* `unselectOnWillPop`: Routeが閉じるときに選択状態をクリアするかどうか。サンプルでは`AppBar`の左に×ボタンがあり、それを押すと`Navigator.maybePop`が呼ばれるので選択状態がクリアされる。デフォルトは`true`。

# ポイント
「選択モード」のトリガーが**ロングタップ**であるため、最初からタップで1つずつ要素を選択することができない。(最初に1つの要素を選択するためには、必ず長押ししないといけない)
>[Issue](https://github.com/hcbpassos/drag_select_grid_view/issues/6)でも同様の質問が上がっていたが、このパッケージの解決したい問題とは異なることから対応はされない模様。あくまでドラッグ選択がメインで、タップ選択はおまけ。

暫定対処として、各要素にタップイベントを仕込み、上記で触れた`DragSelectGridViewController`を使って選択した要素のインデックスを追加することにした。`SelectableItem`に`VoidCallback onTap`を追加し、`example.dart`を以下のように修正。

```dart:example.dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: SelectionAppBar(
        selection: controller.value,
        title: const Text('Grid Example'),
      ),
      body: DragSelectGridView(
        gridController: controller,
        padding: const EdgeInsets.all(8),
        itemCount: 90,
        itemBuilder: (context, index, selected) {
          return SelectableItem(
            index: index,
            color: Colors.blue,
            selected: selected,
            onTap: () {
              final List<int> currentIndexes =
                  controller.value.selectedIndexes.toList();
              if (currentIndexes.contains(index)) {
                currentIndexes.remove(index);
              } else {
                currentIndexes.add(index);
              }
              controller.value = Selection(currentIndexes.toSet());
            },
          );
        },
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 200,
          crossAxisSpacing: 8,
          mainAxisSpacing: 8,
        ),
      ),
    );
```
`controller.value.selectedIndexes.remove()`とかで直接編集しようとすると、`Unsupported operation: Cannot modify an unmodifiable Set`と怒られるので、上記のようにいったん`List`化し`contoller.value`に代入するような形で更新する。