FlutterのListViewでリストを削除した際のA dismissed Dismissible widget is still part of the tree

## はじめに
最近Flutterの勉強を開始しました。
初めてのアプリということで、[ToDoアプリ](https://github.com/kazumaz/flutter-study/tree/master/todo_app)の開発を行っていました。
　
ListViewというwidgetを使っているのですが、途中エラーが発生し困ったので解消法の共有です。
超初心者向けなのであしからず。
　
## 現象
<img width="377" alt="スクリーンショット 2020-01-10 0.03.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513790/a2a0957d-460b-78e1-9534-04f9c3141687.png">

上記のようなTodoリストを作成し、各　TodoはFlipすることで削除できるようにしました。
しかし、test3→test2→test1の順で削除すると、問題なく削除されるのですが、test3を削除する前にtest2を削除するとエラーが発生するという事象に会いました。

## エラー内容

```
Performing hot reload...                                                
Reloaded 1 of 478 libraries in 396ms.
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building Dismissible-[<'0'>](dirty, dependencies:
flutter: [Directionality], state: _DismissibleState#74b1e(tickers: tracking 2 tickers)):
flutter: A dismissed Dismissible widget is still part of the tree.
flutter: Make sure to implement the onDismissed handler and to immediately remove the Dismissible widget from
flutter: the application once that handler has fired.
flutter:
flutter: The relevant error-causing widget was:
flutter:   Dismissible-[<'0'>]
flutter:   file:///Users/kazuma/Desktop/myproject/flutter-study/todo_app/lib/main.dart:53:20
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #0      _DismissibleState.build.<anonymous closure> (package:flutter/src/widgets/dismissible.dart:526:11)
flutter: #1      _DismissibleState.build (package:flutter/src/widgets/dismissible.dart:535:8)
flutter: #2      StatefulElement.build (package:flutter/src/widgets/framework.dart:4334:27)
flutter: #3      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4223:15)
flutter: #4      Element.rebuild (package:flutter/src/widgets/framework.dart:3947:5)
flutter: #5      StatefulElement.update (package:flutter/src/widgets/framework.dart:4413:5)
flutter: #6      Element.updateChild (package:flutter/src/widgets/framework.dart:2977:15)
flutter: #7      SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:545(ddd78475600b5492fc67889427724d06.png)════════════════════════════════════════════════════════════════
```
## エラーとなっているコード
```Dart
    Widget build(BuildContext context) => Scaffold(
          key: _scaffoldKey,
          appBar: AppBar(
            title: Text('Todoリスト'),
          ),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () {
              // Todo入力画面への遷移
              _navigateAndInputTodo(context);
            },
          ),
          body: ListView.builder(
            itemCount: _todos.length, //ここで表示可能なリスト数を制限しないと、表示できなくなった時にエラーになる。
            itemBuilder: (BuildContext context, int index) {
              return Dismissible( //**ここでエラー発生**
                // KeyはFlutterが要素を一意に特定できるようにするための値を設定する。
                key: Key(index.toString()),
                // onDismissedの中にスワイプされた時の動作を記述する。
                // directionにはスワイプの方向が入るため、方向によって処理を分けることができる。
                onDismissed: (direction) {
                  setState(() {
                    // スワイプされた要素をデータから削除する
                    _todos.removeAt(index);
                  });
                  // スワイプ方向がendToStart（画面左から右）の場合の処理
                  if (direction == DismissDirection.endToStart) {
                    Scaffold.of(context).removeCurrentSnackBar();
                    Scaffold.of(context)
                        .showSnackBar(SnackBar(content: Text("削除しました")));
                    // スワイプ方向がstartToEnd（画面右から左）の場合の処理
                  } else {
                    Scaffold.of(context).removeCurrentSnackBar();
                    Scaffold.of(context)
                        .showSnackBar(SnackBar(content: Text("削除しました")));
                  }
                },
                // スワイプ方向がendToStart（画面左から右）の場合のバックグラウンドの設定
                background: Container(
                  alignment: Alignment.centerLeft,
                  color: Colors.redAccent[700],
                  child: Padding(
                      padding: EdgeInsets.fromLTRB(20.0, 0.0, 0.0, 0.0),
                      child: Icon(Icons.delete_forever, color: Colors.white)),
                ),

                // スワイプ方向がstartToEnd（画面右から左）の場合のバックグラウンドの設定
                secondaryBackground: Container(color: Colors.blue),

                child: Card(
                  child: ListTile(
                      title: Text(_todos[index].title),
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                              builder: (context) =>
                                  DetailScreen(todo: _todos[index])),
                        );
                      }),
                ),
              );
            },
          ),
        );
```

## 当時参考にしたサイト

https://stackoverflow.com/questions/55792521/how-to-fix-a-dismissed-dismissible-widget-is-still-part-of-the-tree-error-in
https://stackoverflow.com/questions/58470821/a-dismissed-dismissible-widget-is-still-part-of-the-tree-in-flutter

結局ほとんどのサイトで、`onDismissed`の時に、ちゃんと`setState`でオブジェクトを削除しようってことが書いていたんですよね。
ただ、当時、自分の実装もそうしており、、、下記です。

```Dart
                onDismissed: (direction) {
                  setState(() {
                    // スワイプされた要素をデータから削除する
                    _todos.removeAt(index);
                  });
```

## 結論
`Dismissible`の`key`に`Key(index.toString())`を指定しているのが原因でした。
これだと、途中のTODOが削除された場合、削除したindexが他のTODOで使いまわされてしまい、不整合が起きるので、エラーになっているようです。完全に初心者の過ちでした。。（恥ずかしい）

`Dismissible`の`key`を`ObjectKey(_todos[index])`にするとTODOごとにユニークなキーになるので、エラーが発生しなくなりました。

Flutter、面白いですね。勉強引き続き頑張ります。
これから頑張っていくので、記事に「いいね」いただけるか、githubフォローしていただけるか、Starつけていただけるとモチベーションに繋がるので、もしよかったらよろしくお願いします。:flushed:
https://github.com/kazumaz/flutter-study