[Flutter]BottomNavigationBarを使った下タブのナビゲーションメニューでの画面遷移、ページングを試す

Flutterで下タブにナビゲーションメニューを実装できるBottomNavigationBarについて書きました。
2020.04.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Flutter で画面下部にナビゲーションメニューを配置する時は BottomNavigationBar という Widget を使用できます。基本的な Widget の使い方は覚えて状態管理のスタンダードを軽く学んだので実際にアプリを作り始めたのですがその際に使用した BottomNavigationBar について記事を書きたいと思います。

BottomNavigationBar の基本

1 例として 空の Container から一つずつ進めて作ってみます。以下のコードは Container を返しているだけのコードです。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

実行してみると真っ白になるはずです。

BottomNavigationBar で画面を切り替えるために最低限必要なものを用意します。bottom navigation bar で表示する Widget の一覧と表示中の Widget を取り出すための index としての int 型の mutable な値を定義します。

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // 表示中の Widget を取り出すための index としての int 型の mutable な stored property
  int _selectedIndex = 0;

  // 表示する Widget の一覧
  static List<Widget> _pageList = [
    CustomPage(pannelColor: Colors.cyan, title: 'Home'),
    CustomPage(pannelColor: Colors.green, title: 'Settings'),
    CustomPage(pannelColor: Colors.pink, title: 'Search')
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('サンプル1'),
      ),
      body: _pageList[_selectedIndex],
    );
  }
}

class CustomPage extends StatelessWidget {} // ナビゲーションバーをタップした時に切り替わるWidgetの定義
}

次は StatefulWidget の build メソッドを実装します。Scaffold のコンストラクタ引数 bottomNavigationBar に BottomNavigationBar を渡します。

BottomNavigationBar の引数 currentIndex を mutable property にして引数 onTap に渡したメソッドで setState を呼びその mutable property を更新します。これで画面が切り替わるはずです。

また引数 items にはナビゲーションバーに表示する item(BottomNavigationBarItem)のリストを指定します。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('サンプル1'),
    ),
    body: _pageList[_selectedIndex],
    bottomNavigationBar: BottomNavigationBar(
      items: const <BottomNavigationBarItem>[
        BottomNavigationBarItem(
          icon: Icon(Icons.home),
          title: Text('Home'),
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.settings),
          title: Text('Setting'),
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.search),
          title: Text('Search'),
        ),
      ],
      currentIndex: _selectedIndex,
      onTap: _onItemTapped,
    ),
  );
}

// タップ時の処理
void _onItemTapped(int index) {
  setState(() {
    _selectedIndex = index;
  });
}

コード全文です。codepen 埋め込みです。実行結果も見れますがその場合は Chrome での閲覧を推奨します。

See the Pen bottom_navigation_bar_sample1 by Nobuyuki Tanabe (@nabeatsu) on CodePen.

BottomNavigationBar のドキュメントは以下になります。選択時の見た目の制御を選択できる type プロパティや選択時、非選択時のスタイルを調整できるプロパティもあり、ある程度カスタマイズが効くと思います。

BottomNavigationBar のページングでの画面遷移

このような UI だとページングで画面が切り替わるパターンがあります。それを実現する一つの例としては BottomNavigationBar に加えて PageView、PageController を使用する方法があります。

PageViewController でスワイプを検知してアニメーションさせます。initialPage プロパティを使って開始ページを設定します。PageView のコンストラクタ引数に PageViewController のインスタンスを渡して遷移を実装します。

_selectedIndex を setState で更新しているのでページングによる BottomNavigationItem のハイライトも合わせて反映されます。

ここで終わると BottomNavigationBar の onTap の処理を変更しないと画面遷移の動きに差異が出てしまいます。ボタンタップ時は現状ページングされません。

そのために private プロパティとして PageController のインスタンスを保持しています。

onTap のタイミングで PageController の animateToPage メソッドでページングによる画面遷移を行うよう実装します。

コードは以下です。Chrome の人は Result タブでページングが実装できていることが確認できると思います。

See the Pen bottom_navigation_bar_sample2 by Nobuyuki Tanabe (@nabeatsu) on CodePen.

BottomNavigationBar を使用した場合の Push 遷移で下タブを残したまま遷移したい時

BottomNavigationBar を使った Push 遷移は BottomNavigationBar が隠れてしまいます。Material Design 的にはその動きで問題ないらしいのですが残したまま Push 遷移したいこともあります。iOS アプリの場合は特にそうだと思います。

その際は公式に提供されている CupertinoTabScaffold を使うのが良いと調べた限りでは感じています。

※参考

BottomNavigationBar で同じ動きを実装するワークアラウンドもあります。写経して試しましたが動いた時は感動したもののここまでするなら公式の Widget に乗っかりたいなとも思いました。写経のため自分が書いたような語り口で紹介するのも気が引けるので手順のみ載せて参考記事とリポジトリを紹介します。

  • Navigator.of で BottomNavigationBar の祖先の Navigator を context から見つけてしまう
  • Navigator.of で BottomNavigationBar の祖先に当たらない Navigator を見つけるように Navigator を内包するカスタムウィジェット内に Push の処理を記述する
  • Navigator の識別のために GlobalKey を使う
  • 下タブのタップ時の動きは Stack を使って表示する画面一覧を保持して Offstage を使って任意のタブを Offstage(見えないよう)にして実現する

この実装について紹介した記事は以下になります。

リポジトリは以下。

BottomNavigationBar + provider

状態管理に推奨されている provider というライブラリを使って画面遷移を管理してみた実装です。外部ライブラリを使っているので codepen 埋め込みではないです。

int型のindexををあちこちで引き回したくない気持ちもあったので BottomNavigationBar をラップしたカスタムウィジェットを使って実装したのが以下です。

BottomNavigationというカスタムウィジェットがBottomNavigationBarをラップしています。

他の人のコードを見てもっと良い方法があれば取り入れたいと思います。

まとめ

BottomNavigationBarの挙動制御で表示中のタブをタップした際の動きや別のタブを表示して再度元のタブを表示した時のStateを保持するパターンなど書きたいことは他にもあるのでまたこのWidgetについては別の記事を書く予定です。

最後になりますがこの記事はドキュメントや他の書かれた記事を元に手元で動かしながら書いた内容になっています。きっとより良い方法や説明に不備、誤りがあるかと思います。なにかお気づきの際はコメントやSNS等でご連絡いただければと思います。

最後までありがとうございました。