画面遷移時のHeroタグの重複エラー

# Flutterのページ遷移が上手くいかない...
Flutterで簡易的なTodoアプリを実装しています。

AppBarのactionsに配置したIconButton押下時イベントで別ページへの遷移を実装したかったのですが、あるエラーに惑わされました🤨

![A69CE6C4-EC78-43E4-B6AD-63BD91DA14C5_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3889500/dc28694e-9375-7313-222d-a4fb277b12dc.jpeg)

正確に言うと、画面遷移は可能なのですが遷移するたびに`There are multiple heroes that share the same tag within a subtree`と例外が発生してました。

そのままにしておくのも気持ち悪いので、修正したく調査を実施しました。
エラー解析には下記の記事を参考にさせていただきました。

https://qiita.com/rei_012/items/c07e95b5793d943229e3

# エラーの内容
`There are multiple heroes that share the same tag within a subtree`

翻訳してみると、
「サブツリー内に同じタグを共有するヒーローが複数存在します」となります。

どうやらFlutterのMaterialデザインの実装では、一部の要素に対して暗黙的に**Hero**というWidetを使ったアニメーションが適用されているようです。

**Hero**については奥が深そうなので詳細は言及を省きますが、平たく言うと画面遷移時のアニメーションを実現するために用意されたFlutterフレームワークの一部とのことです。

その**Hero**には遷移元と遷移先を紐付けるための`tag`が存在しており、サブツリー内に同一の`tag`が複数存在する場合に上記のエラーが発生します。

`FloatingActionButton`の実装時によく遭遇するそうですね。
ちなみに`FloatingActionButton`とは、画面下部に表示されているボタンのことです。

# 今回の不具合の原因
今回の問題も`FloatingActionButton`の`tag`が重複していたことが原因でした。

今まではアプリ内にページ遷移の処理が存在しなかったため、`tag`の重複による直接的な影響がなかったようです。だから重複の問題に気づかなかったんですね。

修正前のコードはこんな感じ
(今回の事象と関係のない部分は一部省略してます)

```dart:main_page.dart
/// メイン画面
class MainPage extends HookConsumerWidget {
  const MainPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {    
    return Scaffold(
      drawer:
          // サイドメニューの実装

      // 画面上部のAppBar
      appBar: AppBar(
        backgroundColor: AppColors.main,
        actions: const [
          SearchButton(), // 追加実装した検索ボタン
          SettingsButton(), // 設定ボタン
        ],

      bottom:
        // bottom部の実装

      body:
        // body部の実装
        
      // 問題のfloatingActionButton
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      floatingActionButton: const Padding(
        padding: EdgeInsets.symmetric(horizontal: 15),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            DeleteButton(), // ゴミ箱ボタン
            AddButton(), // 追加ボタン
          ],
        ),
      ),
    );
  }
}
```
```dart:search_button.dart
// 検索のアイコンボタン（押下時にページ遷移したい）
class SearchButton extends StatelessWidget {
  const SearchButton({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: const Icon(Icons.search),
      onPressed: () {
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => const SearchPage()),
        );
      },
    );
  }
}
```
```dart:search_page.dart
// 遷移先のTodo検索ページ
class SearchPage extends StatelessWidget {
  const SearchPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('検索'),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
      body: const Column(
        children: [
          Material(
            color: Colors.transparent,
            child: SearchBar(),
          ),
          // 他の検索関連のウィジェットを実装予定
        ],
      ),
    );
  }
}
```
```dart:delete_button.dart
// ゴミ箱アイコンのfloatingActionButton
class DeleteButton extends ConsumerWidget {
  const DeleteButton({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return FloatingActionButton(
      onPressed: () {
          // ボタン押下時の処理
      },
      
      child: Icon(
          Icons.delete,
      ),
    );
  }
}
```
```dart:add_button.dart
// ＋アイコンのfloatingActionButton
class AddButton extends StatelessWidget {
  const AddButton({super.key});

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: () {
        // ボタン押下時の処理
      },
      
      child: const Icon(
        Icons.add,
      ),
    );
  }
}
```

`SearchButton`クラスのボタン押下時で検索ページへの遷移を実装したところ、`FloatingActionButton`で内部的に使用している**Hero**で`tag`の重複エラーが浮き彫りになったということですかね。

# 修正方法
修正方法は単純でした。

`FloatingActionButton`のプロパティで`heroTag`を明示的に設定し、それぞれの**Hero**に一意性を確保することで修正できました。

修正するのは以下の2つのクラスだけです。
```dart:delete_button.dart
// ゴミ箱アイコンのfloatingActionButton
class DeleteButton extends ConsumerWidget {
  const DeleteButton({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {

    return FloatingActionButton(
      // ここを追加
      heroTag: 'delete_button',
      
      onPressed: () {
          // ボタン押下時の処理
      },
      
      backgroundColor: AppColors.white,
      
      child: Icon(
          Icons.delete,
      ),
    );
  }
}
```
```dart:add_button.dart
// ＋アイコンのfloatingActionButton
class AddButton extends StatelessWidget {
  const AddButton({super.key});

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      // ここを追加
      heroTag: 'add_button',
      
      onPressed: () {
        // ボタン押下時の処理
      },
      
      child: const Icon(
        Icons.add,
      ),
    );
  }
}
```

例外が発生することなくページ遷移できるようになりました🎉

![Simulator Screen Recording - iPhone 15 - 2024-09-29 at 14.51.16.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3889500/cde59b55-4fad-e513-fc1f-d28f31006499.gif)

# 最後に
`FloatingActionButton`が複数存在する場合は**Hero**のタグを個々で設定しないと思わぬ事故の原因となりかねないということですね。

今回は調査方法と修正方法をまとめましたが、Flutter初心者のため認識の相違などがあるかもしれません。

実際にページ遷移可能な処理は実装できましたが、もし間違いやご意見などがございましたらコメントいただけると幸いです🙇‍♂️