単一要素の選択が可能リストをListViewとproviderパッケージで実現する方法とその落とし穴

Flutter は 4 ヶ月目くらいの自称初心者です。アプリの宣伝も兼ねて、アプリ開発とリリースを経てつまづいた点や学びになった点をたまにアウトプットしようと思います。
https://itunes.apple.com/jp/app/id6460978343?mt=8

# まとめ
ListView や Column, Rolw 配下の widget のうちどの要素が選択されているか provider で管理する方法を示す。さらに、要素の indexを保持すると更新のたびに全要素の再ビルドが走るため、各 widget で重たい処理を行う場合必要に応じて各要素ごとに選択されいるかを bool で保持すると良い。


## 導入
ドット絵エディタの Diorite では、ステータスの管理に [provider package](https://pub.dev/packages/provider) を使用しています。
また、エディターでは多数用意された描画用のツールから１つのツールを選択し描画する必要があるため、ListView や Column, Rolw など複数の widget をまとめる widget と provider を使用し、以下のようにアイコンの描画とステータスの管理を行なっていました。

こちらは、最小構成で再現したコードです。
DartPad にコピペすることで簡単に試すことができます。
```dart:main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(ChangeNotifierProvider(
      create: (_) => DrawItemProvider(),
      child: const MaterialApp(
        home: MySample(),
      )));
}

class MySample extends StatelessWidget {
  const MySample({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Row(
        children: context
            .watch<DrawItemProvider>()
            .drawIcons
            .asMap()
            .entries
            .map((e) => GestureDetector(
                onTap: () {
                  context.read<DrawItemProvider>().setDrawId(e.key);
                },
                child: DrawItem(e.key)))
            .toList(),
      )),
    );
  }
}

class DrawItem extends StatelessWidget {
  final index;

  const DrawItem(int this.index, {super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
        margin: const EdgeInsets.all(2),
        width: 40,
        height: 40,
        color:
            (context.select(((DrawItemProvider d) => d.drawId)) == this.index)
                ? Colors.black
                : context.read<DrawItemProvider>().drawIcons[index]);
  }
}

class DrawItemProvider extends ChangeNotifier {
  var drawId = 0;
  List<Color> drawIcons = [Colors.red, Colors.blue, Colors.green];

  void setDrawId(int i) {
    drawId = i;
    notifyListeners();
  }
}


```

以下のようにタップで選択したアイコンに変更を加えることができます。（サンプルでは色を黒に変えています。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254034/bb806912-6c53-e369-d426-8a056420e734.png)


## 発生した問題
使用していたアプリでは、アイコン内部でかなり重たい処理を実行し、解像度の高い画像を表示していました。具体的には、メインのキャンバスで描画している画像の特定レイヤーの見切り出して、画像として表示する処理をしていました。そのため、なるべくアイコンの再描画と再ビルドを避けたく、`debugPrintRebuildDirtyWidgets` フラグをtrueに調べていたところどうやら操作のたびに全アイコンがビルドされているため、この原因調査と修正対応が必要になりました。

## 原因
気づいてしまえば単純な話ですが、原因は各 DrawItem　が同じ drawId を参照していたからでした。

```mermaid
graph TB
subgraph Provider
    id1["int DrawId"]
    id2["List Colors drawIcons"]
end
    DrawItem-#1 --context.select--> id1
    DrawItem-#2 --context.select--> id1
    DrawItem-#3 --context.select--> id1
    DrawItem-#4 --context.select--> id1
```

## 対応
今回は、要素ごとに、選択されたかをステートで持つことで対応しました。
これにより要素数だけ確保する領域は増えるのですが、たかだか Bool が要素数だけ増えるため、要素数に比例して重たい描画処理が増加することに比べると全然許容できるかと思います。


```mermaid
graph TB
subgraph Provider
    id1["int DrawId"]
    id2["List Colors drawIcons"]
    id3["List bool isSelected"]
end
    DrawItem-#1 --context.select--> id3
    DrawItem-#2 --context.select--> id3
    DrawItem-#3 --context.select--> id3
    DrawItem-#4 --context.select--> id3
```


## 対応済みサンプルコード
変更した DrawItem と DrawItemProvider のみ示します。
```dart
class DrawItem extends StatelessWidget {
  final index;

  const DrawItem(int this.index, {super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
        margin: const EdgeInsets.all(2),
        width: 40,
        height: 40,
        color:
            (context.select(((DrawItemProvider d) => d.isSelected[index])))
                ? Colors.black
                : context.read<DrawItemProvider>().drawIcons[index]
    );
  }
}

class DrawItemProvider extends ChangeNotifier {
  var drawId = 0;
  List<Color> drawIcons = [Colors.red, Colors.blue, Colors.green];
  List<bool> isSelected = [true,false,false];

  void setDrawId(int i) {
    isSelected[drawId] = false;
    drawId = i;
    isSelected[i] = true;
    notifyListeners();
  }
}
```

## 最後に
Flutter は最初使い始めた時は特に何も考えなくてもかなり楽に使えるという印象でしたが、パフォーマンスを意識し始めると気にしないといけないことが複数出てくるな〜という印象に変わりました。
しかしながら、パフォーマンスを気にする必要がある、ということはある程度形ができてからになると思うので、アイディアやデザインを高速で形にできるという点で非常に良い言語だなと思います。