【Flutter】BottomNavigationのタブキープ遷移

# 環境+ライブラリ

```:pubspec.yaml
version: 1.0.0+1

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  state_notifier:
  flutter_state_notifier:
  freezed_annotation:
  flutter_riverpod:

dev_dependencies:
  flutter_test:
    sdk: flutter
  json_serializable:
  build_runner:
  freezed:
```

# 実装
```dart:tab_item.dart

enum TabItem { home, list, setting, camera }

const Map<TabItem, String> tabName = {
  TabItem.home: 'home',
  TabItem.list: 'list',
  TabItem.setting: 'setting',
  TabItem.camera: 'camera',
};

const Map<TabItem, IconData> tabIcon = {
  TabItem.home: Icons.home,
  TabItem.list: Icons.list,
  TabItem.setting: Icons.settings,
  TabItem.camera: Icons.camera,
};

```

```dart:bottom_navigation.dart 
class BottomNavigation extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return _BottomNavigationWidget();
  }
}

class _BottomNavigationWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(navigationProvider);
    final provider = watch(navigationProvider.notifier);
    return WillPopScope(
      onWillPop: provider.onWillPop,
      child: Scaffold(
        body: Stack(children: _stackList(state, provider)),
        bottomNavigationBar: CustomBottomNavigationBar(
          currentTab: state.currentTab,
          onSelectTab: provider.selectTab,
        ),
      ),
    );
  }

  List<Widget> _stackList(
      NavigationState state, NavigationStateNotifier notifier) {
    List<Widget> result = [];
    notifier.navigatorKeys.forEach((key, value) {
      result.add(Offstage(
        offstage: state.currentTab != key,
        child: TabNavigator(
          navigatorKey: value,
          tabItem: key,
        ),
      ));
    });
    return result;
  }
}

```
```dart:custom_bottom_navigation_bar.dart
class CustomBottomNavigationBar extends StatelessWidget {
  CustomBottomNavigationBar(
      {required this.currentTab, required this.onSelectTab});
  final TabItem currentTab;
  final ValueChanged<TabItem> onSelectTab;

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      type: BottomNavigationBarType.fixed,
      items: [
        _buildItem(TabItem.home),
        _buildItem(TabItem.list),
        _buildItem(TabItem.setting),
        _buildItem(TabItem.camera),
      ],
      onTap: (index) => onSelectTab(TabItem.values[index]),
    );
  }

  BottomNavigationBarItem _buildItem(TabItem tabItem) {
    return BottomNavigationBarItem(
      icon: Icon(
        _tabIcon(tabItem),
        color: _tabColor(tabItem),
      ),
      label: '',
    );
  }

  Color _tabColor(TabItem item) {
    return currentTab == item ? Colors.blue : Colors.grey;
  }

  IconData _tabIcon(TabItem item) {
    return tabIcon[item]!;
  }
}

```
```dart:tab_navigator.dart 
class TabNavigator extends StatelessWidget {
  TabNavigator({required this.navigatorKey, required this.tabItem});
  final GlobalKey<NavigatorState> navigatorKey;
  final TabItem tabItem;
  final String root = '/';

  Map<String, WidgetBuilder> _routeBuilders(BuildContext context) {
    Map<String, WidgetBuilder> value;
    switch (tabItem) {
      case TabItem.home:
        value = {root: (context) => HomePage()};
        break;
      case TabItem.list:
        value = {root: (context) => ListPage()};
        break;
      case TabItem.setting:
        value = {root: (context) => SettingPage()};
        break;
      case TabItem.camera:
        value = {root: (context) => CameraPage()};
        break;
    }
    return value;
  }

  @override
  Widget build(BuildContext context) {
    final routeBuilders = _routeBuilders(context);
    return Navigator(
      key: navigatorKey,
      initialRoute: root,
      onGenerateRoute: (routeSettings) {
        return MaterialPageRoute(
          builder: (context) => routeBuilders[routeSettings.name]!(context),
        );
      },
    );
  }
}

```
```dart:navigation_state.dart
part 'navigation_state.freezed.dart';

final navigationProvider =
    StateNotifierProvider((_) => NavigationStateNotifier());

@freezed
abstract class NavigationState with _$NavigationState {
  const factory NavigationState({
    @Default(TabItem.home) TabItem currentTab,
  }) = _NavigationState;
}

class NavigationStateNotifier extends StateNotifier<NavigationState> {
  NavigationStateNotifier() : super(const NavigationState());

  final navigatorKeys = {
    TabItem.home: GlobalKey<NavigatorState>(),
    TabItem.list: GlobalKey<NavigatorState>(),
    TabItem.setting: GlobalKey<NavigatorState>(),
    TabItem.camera: GlobalKey<NavigatorState>(),
  };

  Future<bool> onWillPop() async {
    var _currentTab = state.currentTab;
    final isFirstRouteInCurrentTab =
        !await navigatorKeys[_currentTab]!.currentState!.maybePop();

    if (isFirstRouteInCurrentTab) {
      if (_currentTab != TabItem.home) {
        selectTab(TabItem.home);
        return false;
      }
    }
    return isFirstRouteInCurrentTab;
  }

  void selectTab(TabItem tabItem) {
    if (tabItem == state.currentTab) {
      navigatorKeys[tabItem]!.currentState!.popUntil((route) => route.isFirst);
    } else {
      state = state.copyWith(currentTab: tabItem);
    }
  }
}


```

# 注意
各種タブの画面でのダイアログ表示ではrootNavigatorをtrueで設定する