【Flutter】NavigationBarを残しつつボタンで画面遷移

flutter

BotttomNavigationBarを設定したアプリにおい下の BottomBNavBarを残したまま画面のボタンで遷移するやり方について理解したく備忘録として残します。(2023.03現在)

Flutterバージョン

Flutter 3.0.5
Tools • Dart 2.17.6 • DevTools 2.12.2

完成図

必要なライブラリ

今回はHooksを使用して状態管理を行います。
現在のバージョンを公式からコピしてpubspec.yamlに記載。

flutter_hooks | Flutter package
A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.

TabBarに必要なものを宣言

NavigaitoBarとは別のファイルに必要なものを宣言(同じファイル内でも問題ないですが、可読性のためファイルを分けました)

列挙型で中身は二つのタブの名前、アイコン、本体のインスタンスです。

// 列挙型で各タブのタイトル、アイコン、ページのインスタンスを宣言
enum TabItem {
  home(
    title: 'ホーム',
    icon: Icons.home,
    page: HomePage(),
  ),

favorite(
   title: 'お気に入り',
   icon: Icons.favorite,
   page: SecondPage(),
  );

  // 上のhome(),favorite()の引数を宣言し引数として使えるように設定
  // 参考サイト: https://www.fuwamaki.com/article/380
const TabItem({
   required this.title,
   required this.icon,
   required this.page,
  });

    final String title;
    final IconData icon;
    final Widget page;
}

ホームページ

メインのホームページはボタンを配置しただけの簡単なものです。
繊維の仕方もオーソドックスなNavigator.of()の遷移です。

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('home'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('テストページへ'),
          onPressed: () {
            Navigator.of(context).push(MaterialPageRoute(builder: (context) {
              return TestPage();
            }));
          },
        ),
      ),
    );
  }
}

遷移先のページ

遷移先の注意点としてScaffoldウィジェットでappbarを宣言しないと戻るボタンが表示されません。

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar( // アプリバーを設定しないと戻るボタンが表示されない
          title: Text('test')
      ),
      body: Center(
          child: Text('テストページ')
      ),
    );
  }
}

BottomNavigationBarを管理するメインページ

一番重要な部分です。大事な部分を解説していこうと思います。

ページ全体


// Map型でTabITemに対して一つのキーをもつデータコレクションを用意
final _navigatorKeys = <TabItem, GlobalKey<NavigatorState>>{
  TabItem.home: GlobalKey<NavigatorState>(), //ホームページ用のグローバルキー
  TabItem.favorite: GlobalKey<NavigatorState>(),//お気に入りページ用のグローバルキー
};

// HookWidgetで状態管理
class MainApp extends HookWidget {
  const MainApp({super.key});


  @override
  Widget build(BuildContext context) {
    // useState で選択状態の管理 初期値をhomeに設定
    // 参考サイト: https://qiita.com/mkosuke/items/f88419d0f4d41ed6d858
    final currentTab = useState(TabItem.home);
    return MaterialApp(
      home: Scaffold(
        body: Stack( // スタックWidgetを使って状態を保つ
          // Offstageは表示/非表示を設定したいWidgetをchildに入れる
          // enumのvaluesは全ての配列ケースを取得できるもの 
          children: TabItem.values.map((tabItem) => Offstage(
              offstage: currentTab.value != tabItem,
              //各ページの Navigator に NavigatorState を持った Key を渡す
              child: Navigator(
                key: _navigatorKeys[tabItem],
                onGenerateRoute: (settings) {
                  return MaterialPageRoute<Widget>(
                    builder: (context) => tabItem.page,
                  );
                },
              ),
            ),
          ).toList(),
        ),
        bottomNavigationBar: BottomNavigationBar(
          type: BottomNavigationBarType.fixed,
          currentIndex: TabItem.values.indexOf(currentTab.value),
          items: TabItem.values.map((tabItem) => BottomNavigationBarItem(
              icon: Icon(tabItem.icon),
              label: tabItem.title,
            ),
          ).toList(),
          onTap: (index) {
            // ③ 選択済なら第一階層まで pop / 未選択なら currentTab に指定
            final selectedTab = TabItem.values[index];
            if (currentTab.value == selectedTab) {
              _navigatorKeys[selectedTab]
                  ?.currentState
                  ?.popUntil((route) => route.isFirst);
            } else {
              currentTab.value = selectedTab;
            }
          },
        ),
      ),
    );
  }
}

グローバルキーを用意

各ページのグローバルキーを宣言します。

Flutter Widget Keyの種類と使い方について - Qiita
はじめにFlutterで時々利用するWidget の Keyについてのまとめです。Keyが必要な理由と仕組みについて (1回目)主にKeyとは何か?、そもそもKeyが必要になる理由についてまと…
// Map型でTabITemに対して一つのキーをもつデータコレクションを用意
final _navigatorKeys = <TabItem, GlobalKey<NavigatorState>>{
  TabItem.home: GlobalKey<NavigatorState>(), //ホームページ用のグローバルキー
  TabItem.favorite: GlobalKey<NavigatorState>(),//お気に入りページ用のグローバルキー
};

useStateで状態管理

flutter_hooksのuseStateを使用して最初のページにホームページを設定できるように宣言します。

    final currentTab = useState(TabItem.home);

bodyの中身

このアプリのbodyであるStackWidgteの中身です。
私が重要かなと思う以下の3つのポイントについて解説していきます。

①map.toListで表示
②Offstage
③Navigatorで管理

          children: TabItem.values.map((tabItem) => Offstage(
              offstage: currentTab.value != tabItem,
              //各ページの Navigator に NavigatorState を持った Key を渡す
              child: Navigator(
                key: _navigatorKeys[tabItem],
                onGenerateRoute: (settings) {
                  return MaterialPageRoute<Widget>(
                    builder: (context) => tabItem.page,
                  );
                },
              ),
            ),
          ).toList(),

①map.toListで表示

map.toList()で表示することによって自分が表示したいアイテムをループ文を使用せずにシンプルに実装できます。

【Flutter】map() & toList()メソッドを使用して、Widgetを表示する|Flutterラボ
変数宣言 List<String> list = ; この3つのStringを使用して動的にWidgetをしてリスト表示してみましょう。 ループ文を使用して表示する List<Widget> buildItems(){ List<Widget> items = []; list.forEach((...

②Offstage

Offstageウィジェットはウィジェットの表示、非表示を管理できるウィジェットです。
表示、非表示を管理してcurrentTabのvalueが引数のtabItemでなければ非表示にしています。

[Flutter] OffstageとStackでWidgetを切り替える - Qiita
##はじめにこの記事では以下の画像のような,ボタンを押すことでWidgetを切り替える方法について説明しています。 ##この記事が参考になるかもしれない人コピペで動くソースコードをいじること…

③Navigatorで管理

Navigatorは状態管理に使用するWidgteです。
各ページのNavigatorに最初に宣言したkeyを渡しています。

  key: _navigatorKeys[tabItem],

そしてルートに各タブを渡して遷移できるようにしてきます。

                onGenerateRoute: (settings) {
                  return MaterialPageRoute<Widget>(
                    builder: (context) => tabItem.page,
                  );
                },
Navigator class - widgets library - Dart API
API docs for the Navigator class from the widgets library, for the Dart programming language.

BottomNavigationBar

あとは、BottomNavigationBarを設定していきます。
ここで重要なポイントは下記の3つです。

①現在のページ設定
②BottomNavigationBarのitem設定
③タップした際の処理

BottomNavigationBar(
          type: BottomNavigationBarType.fixed,
          currentIndex: TabItem.values.indexOf(currentTab.value),
          items: TabItem.values.map((tabItem) => BottomNavigationBarItem(
              icon: Icon(tabItem.icon),
              label: tabItem.title,
            ),
          ).toList(),
          onTap: (index) {
            // 選択済なら第一階層まで pop / 未選択なら currentTab に指定
            final selectedTab = TabItem.values[index];
            if (currentTab.value == selectedTab) {
              _navigatorKeys[selectedTab]
                  ?.currentState
                  ?.popUntil((route) => route.isFirst);
            } else {
              currentTab.value = selectedTab;
            }
          },
     ),

①現在のページ設定

現在のページ(今いるページ)には、currentTabのvalueつまりuseStateで取得した情報をindexOfで設定します。

currentIndex: TabItem.values.indexOf(currentTab.value),

②BottomNavigationBarのitem設定

BottomNavigationBarのアイテムもMap.tolistで設定しています。
引数のtabItemからそれぞれもアイコンとタイトルを設定しています。

③タップした際の処理

まず、引数index番目のTabItem(列挙型でまとめたページの中身)を取得できるように宣言。

            final selectedTab = TabItem.values[index];

BottomNavigationBarをタップした際の処理です。

useStateで取得したもの(currentTab.value)と今、選択されてるタブが同じならルートの最初つまり選択したページにpop(戻る処理)をする。
そうでなければuseStateで取得したもの(currentTab.value)に今、選択されているタブを代入するというイメージです。

onTap: (index) {
            if (currentTab.value == selectedTab) {
              _navigatorKeys[selectedTab]
                  ?.currentState
                  ?.popUntil((route) => route.isFirst);
            } else {
              currentTab.value = selectedTab;
            }
          },

参考サイト

下記サイトを参考に不明点を自分なりにまとめてみました。非常に参考になるのでこちらもチェックしてみてください。

【Flutter】BottomNavigationBar 永続化の最小サンプル作ってみた

最後に

Hooksを使っての遷移について記載しましたが、曖昧な部分もありますので気になる点があればぜひフィードバックをいただけると幸いです。

ではまた。

コメント

タイトルとURLをコピーしました