[flutter] WidgetKeyについて深掘りしてみた

## はじめに

普段何気なく使っている「GlobalKey」が実際何のために使用されており、
どういう挙動を実現しているのか興味が湧いたので深掘りして調べてみました。
もしどなたかの参考になれば嬉しいと思い、記事にしてみました。

今回の記事では、GlobalKeyを例に、
BottomTabBarの実装を例にKey(GlobalKey)がどのような役割を担っているのか学んでいければと思います。

## keyとは何かドキュメントを読んでみる。

https://api.flutter.dev/flutter/foundation/Key-class.html

> Keys must be unique amongst the Elements with the same parent.
 Subclasses of Key should either subclass LocalKey or GlobalKey.

 keyは同じ親を共有するelements内でユニークな値である必要があり、
 LocalKeyとGlobalKeyは必ずKeyクラスを継承しなければいけないみたいですね。

 Key自体の役割は、ツリー状に構成されているElementの中から、
 その要素の一部である、Element要素にユニークなWidgetを識別するために使用します。

 ListWidgetのスクロール位置の保存など、
 BottomTabBarで特定のWidgetに遷移する際に使用したりするのが一般的なのではないでしょうか？

 ## LocalKeyとGlobalKeyの違い

 LocalKeyは、**同じ親を共通とする子のWidget間で子Widget同士を識別するため**に必要なKeyです。
GlobalKeyは、文字通りLocalKeyとは異なり、
**全く別のWidgetTreeのWidgetを識別したいとき**に使用します。



例えば、以下のColumnの中のText同士を識別させたい場合は、
LocalKeyを使用します。

![スクリーンショット 2023-10-29 11.53.58 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3158219/8ccbe568-9b8c-e496-63b1-431e74b1bac2.png)


Rowの中のTextWidgetから全く別のColumnの子WIdgetを識別したい場合に採用するのは、
GlobalKeyといった具合になります。


 ![スクリーンショット 2023-10-29 11.59.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3158219/fd31ddcc-f3e6-6636-0977-1b8dabda43a4.png)


 ## GlobalKeyでの使用例

 今回はGlobalKeyで実際にBottomTabBarで使用されている例を見つつ、
 理解を深めていきましょう。

 今回は、以下画像のような一般的なBottomTabBarを考えます。

 ![スクリーンショット 2023-10-31 22.01.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3158219/81c1a7d0-91d5-65eb-0584-f95735dfa979.png)


 enum型でタブの数を管理していて、
 enumの数だけ、**_navigatorKeys**でGlobalKeyを生成しています。
 
```dart

enum TabItem {
  a,
  b,
  c,
  d,
  e,

}

  final Map<TabItem, GlobalKey<NavigatorState>> _navigatorKeys = {
  
    TabItem.a: GlobalKey<NavigatorState>(),
    TabItem.b: GlobalKey<NavigatorState>(),
    TabItem.c: GlobalKey<NavigatorState>(),
    TabItem.d: GlobalKey<NavigatorState>(),
    TabItem.e: GlobalKey<NavigatorState>(),
   

  };

```

上記で作成した_navigatorKeysをTabNavigatorのnavigationKeyのパラメータで指定しています。



```dart

  Widget _buildTabItem(
      TabItem tabItem,
      String root,
      ) {
    return Offstage(
      offstage: ref.watch(currentTab) != tabItem,
      child: TabNavigator(
        navigationKey: _navigatorKeys[tabItem]!,
        tabItem: tabItem,
        routerName: root,
      ),
    );
  }
```

※全てのコードを記載するとかなりコード量が大きくなってしまうのと、
今回はあくまでKeyの理解が趣旨なので省略させて頂きます。
気になる方は以下のコードも参照してみてください。

<details><summary>MyAppクラスはこちら</summary>

```dart
class MyApp extends ConsumerStatefulWidget {
   const MyApp({super.key});


   @override
   HomeViewState createState() => HomeViewState();
}


class HomeViewState extends ConsumerState<MyApp> {
  @override
  void initState() {
    super.initState();
 
  }

  @override
  Widget build(BuildContext context) {


    return MaterialApp(
        home: Scaffold(
          body:  Stack(
        children: <Widget>[
        
         _buildTabItem(
           TabItem.a,
           '/a',
          ),
        
         _buildTabItem(
           TabItem.b,
           '/b',
          ),
    
          _buildTabItem(
            TabItem.c,
            '/c',
          ),
          _buildTabItem(
            TabItem.d,
            '/d',
          ),
          _buildTabItem(
            TabItem.e,
            '/e',
          ),
       



      ],
    ),
    bottomNavigationBar: BottomNavigation(
    currentTab: ref.watch(currentTab),
    onSelect: onSelect,
    ),

        )

    );
  }




  Widget _buildTabItem(
      TabItem tabItem,
      String root,
      ) {
    return Offstage(
      offstage: ref.watch(currentTab) != tabItem,
      child: TabNavigator(
        navigationKey: _navigatorKeys[tabItem]!,
        tabItem: tabItem,
        routerName: root,
      ),
    );
  }

}

```
</details>



<details><summary>TabNavigatorクラスはこちら</summary>

```dart

class TabNavigator extends StatelessWidget {
  const TabNavigator({
    super.key,
    required this.tabItem,
    required this.routerName,
    required this.navigationKey,

  });

  final TabItem tabItem;
  final String routerName;
  final GlobalKey<NavigatorState> navigationKey;

  Map<String, Widget Function(BuildContext)> _routerBuilder(BuildContext context) => {
  
    '/a': (context) => const TopView(),
    '/b': (context) => const CalendarViews(),
    '/c': (context) => const UserReportMainView(),
    '/d': (context) => const AlarmView(),
    '/e': (context) =>  const MyPageView()

  };

  @override
  Widget build(BuildContext context) {
    final routerBuilder = _routerBuilder(context);

    return Navigator(
      key: navigationKey,
      initialRoute: '/',
      onGenerateRoute: (settings) {
        return MaterialPageRoute<Widget>(
          builder: (context) {
            return routerBuilder[routerName]!(context);
          },
        );
      },
    );
  }
}

```
</details>

## なぜGlobalKeyが必要なのか。

Flutter の BottomNavigationBarは、
各画面上から新しい画面に遷移していくと、
遷移前の画面から遷移後の画面をスタックしていくのですが、
その際にBottomNavigationBarが画面から消えてしまうという挙動になっています。

(気になる方は以下の記事を参照してみてください。)


https://zenn.dev/flutteruniv_dev/articles/nested_bottom_nav

ですが、各画面のGlobalKeyを、参照させることにより、
その画面からStackされた画面もGlobalKeyから**Elementから管理することが可能**になります。

このように、他のwidgetを操作している際に、
**他のWidgetTreeの各要素のデータを参照する必要がある場合などにKeyが必要になる**んですね。


## 参考資料

https://api.flutter.dev/flutter/foundation/Key-class.html

https://zenn.dev/flutteruniv_dev/articles/nested_bottom_nav

https://qiita.com/kurun_pan/items/f91228cf5c793ec3f3cc

https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c

## 最後に

最後までご覧いただきありがとうございました。
何かご指摘等あればコメントいただけると幸いです。

ありがとうございました！