【Flutter】GetXを利用してBottomNavigatonのタブごとにナビゲーションを切り分ける方法

こんにちは、株式会社Pentagonでアプリ開発をしている石渡港です。
https://pentagon.tokyo

今回は、FlutterでGetXを利用してBottomNavigatonのタブごとにナビゲーションを切り分ける方法について調査しました。
その結果、Get.nestedKeyGet.toNamedにIdを与えると、実現できることがわかりました。
また、Get.argmentの取得の際にGetPageRoutesettingsNavigatoronGenerateRouteで取得するsettingsを設定する必要があります。今回の記事のソースコードには実装していませんが、同一ナビゲーション内で戻る場合に、WillPopScopeを利用してGet.backする際はidを渡す必要があるようです。

【この記事を読むメリット】

  • FlutterでGetXを利用して、下記の画像のようなナビゲーションを切り分ける方法がわかります。

【こんな方に参考にしていただきたい】

  • FlutterでGetXを利用している人。

【調査の動機】
Flutterで普段利用しているGetXを利用して、タブごとのナビゲーションを切り分ける方法を知りたかったため。

【調査結果】

  • Get.nestedKeyNavigatorに設定する必要があります。
  • Get.argmentsを取得するためには、GetPageRouteのsettingsNavigatoronGenerateRouteで取得するsettingsの設定が必須です。
  • Get.toNamedなどで遷移する際にidを渡さなければなりません。また、idを指定しない場合、BottomNavigatonが存在しているナビゲーション上で遷移することがわかりました。
  • 今回の記事のソースコードには実装していませんが、同一ナビゲーション内で戻る場合に、WillPopScopeを利用してGet.backする際はidを渡す必要があるようです。
目次

【結論】

GetXを利用してBottomNavigatonのタブごとにナビゲーションを切り分ける方法

gifで示すようにナビゲーションを切り分けられます。
以下のソースコードを書くことで実装できます。

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'タブごとのナビゲーション切り替え',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const BottomNavigationScreen(),
    );
  }
}

class BottomNavigationScreen extends StatefulWidget {
  const BottomNavigationScreen({Key? key}) : super(key: key);

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

class _BottomNavigationScreenState extends State<BottomNavigationScreen> {
  int _selectedIndex = 0;

  static final List<Widget> _pageList = [
    const HomeNavigator(
      key: PageStorageKey<String>("home"),
    ),
    const SettingsNavigator(
      key: PageStorageKey<String>("setting"),
    ),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: _pageList.map((e) {
          final idx = _pageList.indexOf(e);
          // タブ切り替え時に毎回再読み込みが発生する為に、非表示で保持する
          return Visibility(
            visible: _selectedIndex == idx,
            maintainState: true,
            child: e,
          );
        }).toList(),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          const BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'ホーム',
          ),
          const BottomNavigationBarItem(
            icon: Icon(
              Icons.menu_outlined,
            ),
            label: '設定',
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
      ),
    );
  }
}

final homeId = 1;

class HomeNavigator extends StatelessWidget {
  const HomeNavigator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: Get.nestedKey(homeId),
      initialRoute: "/",
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case "/":
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "/",
              page: () => const HomeScreen(),
            );
          case "/a_screen":
            Get.routing.args = settings.arguments;
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "a_screen",
              page: () => const CommonScreen(),
            );
          default:
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "/progress",
              page: () => CircularProgressIndicator(),
            );
        }
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ホーム'),
      ),
      body: SafeArea(
        child: Center(
          child: MaterialButton(
            child: const Text('Aスクリーンへ'),
            onPressed: () {
              Get.toNamed(
                '/a_screen',
                id: homeId,
                arguments: 'Aスクリーン',
              );
            },
          ),
        ),
      ),
    );
  }
}

class CommonScreen extends StatelessWidget {
  const CommonScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final title = Get.arguments as String;
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
    );
  }
}

final settingsId = 2;

class SettingsNavigator extends StatelessWidget {
  const SettingsNavigator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: Get.nestedKey(settingsId),
      initialRoute: "/",
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case "/":
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "/",
              page: () => const SettingsScreen(),
            );
          case "/b_screen":
            Get.routing.args = settings.arguments;
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "b_screen",
              page: () => const CommonScreen(),
            );
          default:
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "/progress",
              page: () => CircularProgressIndicator(),
            );
        }
      },
    );
  }
}

class SettingsScreen extends StatelessWidget {
  const SettingsScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('設定'),
      ),
      body: SafeArea(
        child: Center(
          child: MaterialButton(
            child: const Text('Bスクリーンへ'),
            onPressed: () {
              Get.toNamed(
                '/b_screen',
                id: settingsId,
                arguments: 'Bスクリーン',
              );
            },
          ),
        ),
      ),
    );
  }
}

Navigatorの設定方法

key: Get.nestedKey(homeId),Navigatoridkeyを設定します。
また、Get.argmentsで変数を受け渡したい場合は下記のような設定が必要になります。

...抜粋
            return GetPageRoute<dynamic>(
              settings: settings,
...抜粋
...省略
final homeId = 1;

class HomeNavigator extends StatelessWidget {
  const HomeNavigator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: Get.nestedKey(homeId),
      initialRoute: "/",
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case "/":
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "/",
              page: () => const HomeScreen(),
            );
          case "/a_screen":
            Get.routing.args = settings.arguments;
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "a_screen",
              page: () => const CommonScreen(),
            );
          default:
            return GetPageRoute<dynamic>(
              settings: settings,
              routeName: "/progress",
              page: () => CircularProgressIndicator(),
            );
        }
      },
    );
  }
}

遷移の方法

下記のようにGet.toNamedidを指定することで、Navigatorを指定したタブ内で遷移できます。

...抜粋
              Get.toNamed(
                '/a_screen',
                id: homeId,
...抜粋
...省略
class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ホーム'),
      ),
      body: SafeArea(
        child: Center(
          child: MaterialButton(
            child: const Text('Aスクリーンへ'),
            onPressed: () {
              Get.toNamed(
                '/a_screen',
                id: homeId,
                arguments: 'Aスクリーン',
              );
            },
          ),
        ),
      ),
    );
  }
}
...省略

また、ナビゲーションを指定してGet.backする際に、WillPopScopeを利用している場合は下記のように指定してください。

...省略
WillPopScope(
  onWillPop: () async {
    Get.back<void>(id: homeId);
    return false;
  },
...省略

【まとめ】

Flutterで普段利用しているGetXを利用して、タブごとのナビゲーションを切り分ける方法がわかりました。
nestedKeyやID、settingを利用することで容易に実装できました。
Getを利用するパターンの方法が、日本語のリファレンスだと少なかったので参考になれば嬉しいです。
この方法で、UXがよくなると思うので、うまく利用していきたいです。

採用情報はこちら
目次