【Flutter】「There are multiple heroes that share the same tag within a subtree」の不具合と戦ってみた

![スクリーンショット 2024-12-03 21.51.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/672609/c040a706-e676-59b6-54ef-8ef313462d94.png)


# エラーになるまでの簡単な流れ
- `bottomNavigationBar`を使って実装していた
- それぞれのタブの切り替えをした時に再描画されるのが嫌で、修正してみた
- 修正してみたところ、Exceptionが発生した


# エラーになるまでを詳しく

### 1. 以下のようなタブ画面を作成していた (作成しているアプリから抜粋)
```dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:food_gram_app/ui/screen/tab/tab_view_model.dart';

class TabScreen extends ConsumerWidget {
  const TabScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(tabViewModelProvider());
    final controller = ref.watch(tabViewModelProvider().notifier);
    return Scaffold(
      body: controller.pageList[state.selectedIndex],
      bottomNavigationBar: SizedBox(
        height: 80,
        child: Theme(
          data: Theme.of(context).copyWith(
            splashColor: Colors.transparent,
            highlightColor: Colors.transparent,
          ),
          child: BottomNavigationBar(
            currentIndex: state.selectedIndex,
            onTap: controller.onTap,
            items: <BottomNavigationBarItem>[
              BottomNavigationBarItem(
                icon: Padding(
                  padding: const EdgeInsets.only(top: 18),
                  child: Icon(
                    Icons.fastfood_outlined,
                    semanticLabel: 'timelineIcon',
                  ),
                ),
                label: '',
              ),
              BottomNavigationBarItem(
                icon: Padding(
                  padding: const EdgeInsets.only(top: 18),
                  child: Icon(
                    CupertinoIcons.map,
                    semanticLabel: 'mapIcon',
                  ),
                ),
                label: '',
              ),
              BottomNavigationBarItem(
                icon: Padding(
                  padding: const EdgeInsets.only(top: 18),
                  child: Icon(
                    CupertinoIcons.profile_circled,
                    semanticLabel: 'profileIcon',
                  ),
                ),
                label: '',
              ),
              BottomNavigationBarItem(
                icon: Padding(
                  padding: const EdgeInsets.only(top: 18),
                  child: Icon(
                    Icons.settings,
                    semanticLabel: 'settingIcon',
                  ),
                ),
                label: '',
              ),
            ],
            type: BottomNavigationBarType.fixed,
            iconSize: 28,
            elevation: 0,
            backgroundColor: Colors.white,
            selectedItemColor: Colors.black,
            unselectedItemColor: Colors.grey,
            selectedFontSize: 0,
            unselectedFontSize: 0,
          ),
        ),
      ),
    );
  }
}

```

### ２.それぞれのタブの切り替えをした時に再描画されるのが嫌だったため、修正した

- IndexedStack を使用すると、タブ切り替え時に他のタブの状態を保持しつつ、現在のタブだけが表示される

```dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:food_gram_app/ui/screen/tab/tab_view_model.dart';

class TabScreen extends ConsumerWidget {
  const TabScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(tabViewModelProvider());
    final controller = ref.watch(tabViewModelProvider().notifier);
    return Scaffold(
      /// タブ切り替え時に他のタブの状態を保持しつつ、現在のタブだけが表示される 
      body: IndexedStack(
        index: state.selectedIndex,
        children: controller.pageList,
      ),
      bottomNavigationBar: SizedBox(
        height: 80,
        child: Theme(
          data: Theme.of(context).copyWith(
            splashColor: Colors.transparent,
            highlightColor: Colors.transparent,
          ),
          child: BottomNavigationBar(
            currentIndex: state.selectedIndex,
            onTap: controller.onTap,
            items: <BottomNavigationBarItem>[
              BottomNavigationBarItem(
                icon: Padding(
                  padding: const EdgeInsets.only(top: 18),
                  child: Icon(
                    Icons.fastfood_outlined,
                    semanticLabel: 'timelineIcon',
                  ),
                ),
                label: '',
              ),
              BottomNavigationBarItem(
                icon: Padding(
                  padding: const EdgeInsets.only(top: 18),
                  child: Icon(
                    CupertinoIcons.map,
                    semanticLabel: 'mapIcon',
                  ),
                ),
                label: '',
              ),
              BottomNavigationBarItem(
                icon: Padding(
                  padding: const EdgeInsets.only(top: 18),
                  child: Icon(
                    CupertinoIcons.profile_circled,
                    semanticLabel: 'profileIcon',
                  ),
                ),
                label: '',
              ),
              BottomNavigationBarItem(
                icon: Padding(
                  padding: const EdgeInsets.only(top: 18),
                  child: Icon(
                    Icons.settings,
                    semanticLabel: 'settingIcon',
                  ),
                ),
                label: '',
              ),
            ],
            type: BottomNavigationBarType.fixed,
            iconSize: 28,
            elevation: 0,
            backgroundColor: Colors.white,
            selectedItemColor: Colors.black,
            unselectedItemColor: Colors.grey,
            selectedFontSize: 0,
            unselectedFontSize: 0,
          ),
        ),
      ),
    );
  }
}

```

### ３.以下のエラーが発生した

- 「There are multiple heroes that share the same tag within a subtree」が発生した

```
======== Exception caught by scheduler library =====================================================
The following assertion was thrown during a scheduler callback:
There are multiple heroes that share the same tag within a subtree.

Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), each Hero must have a unique non-null tag.
In this case, multiple heroes had the following tag: <default FloatingActionButton tag>
Here is the subtree for one of the offending heroes: Hero
  tag: <default FloatingActionButton tag>
  state: _HeroState#f3a81
When the exception was thrown, this was the stack: 

以下省略...
```

### ４.とりあえず治ったので共有

- `FloatingActionButton`の`heroTag`を無効化することによって不具合解消

```dart
import 'package:flutter/material.dart';

class AppFloatingButton extends StatelessWidget {
  const AppFloatingButton({
    required this.onTap,
    super.key,
  });

  final Function() onTap;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 70,
      height: 70,
      child: FloatingActionButton(
        heroTag: null,
        foregroundColor: Colors.black,
        backgroundColor: Colors.black,
        elevation: 10,
        shape: CircleBorder(side: BorderSide()),
        onPressed: onTap,
        child: const Icon(
          Icons.add,
          color: Colors.white,
          size: 35,
        ),
      ),
    );
  }
}

```

### まとめ
- ウィジェットツリー内で同じ Hero タグ（tag）を持つ複数の Hero ウィジェットが存在し、それらが同じページ（PageRoute）内で競合しているそう
- Hero ウィジェットは、画面遷移時にアニメーションを行うための仕組みであり、その仕組みでは tag を用いて対応するウィジェットを特定する。そのため、同じ tag が使われているとどちらをアニメーションさせるべきか分からなくなり、エラーになるらしい
- 今回は、タブの画面のうちFloatingActionButtonを二回使っているため、重複してしまった



# 最後に
MapLibreを使用したアプリを個人で開発しています･:*+.\(( °ω° ))/.:+

![Group 93.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/672609/318bb611-4e87-6c02-6a2c-2c5bb93c392a.png)



https://apps.apple.com/jp/app/foodgram/id6474065183


- 「FoodGram」は、フードシェアアプリとなっており、あなたの好きなレストランの食事をぜひこのアプリで共有していただけると嬉しいです!!

<b>このアプリのセールスポイント
- このアプリだけのレストランマップをユーザー全員で作成できる⭐️
- 自分だけのフードアルバムを構築できる⭐️
- 世界中の人たちの投稿を自由に閲覧できる⭐️