🌏

FlutterのBottomNavigationBarを本気で学ぶ

9 min read

この記事について

本気で学ぶシリーズ第2弾です。公式ドキュメントを参考に、初学者から中級者に向けてわかりやすく解説出来ればと思います。指摘事項や助言などは歓迎です。もしございましたら、Twitterもしくは記事コメントまでお願い致します。

BottomNavigationBarの基礎

LINEやTwitterなど、最近のアプリケーションデザインはTopNavBar(TabBar)ではなくBottomNavbarを導入している所が多いですね。FlutterでもBottomNavBarの書き方を学んでいきましょう。

BottomNavigationBarクラスはScaffoldのbottomNavigationBarプロパティに配置します。
そして、BottomNavigationBarは以下の必須条件が含まれます。

・ページ(NavItems)が二つ以上含まれている
・それらのページのアイコンはnullには出来ない
・それらのページのTitle(label)はnullには出来ない

また、FlutterのBottomNavigationBarはMaterialDesignを元に構成されています。詳しいデザインなどは以下の公式HPを参考にしてください。

https://material.io/components/bottom-navigation

BottomNavigationBarのプロパティ

BottomNavigationBarウィジェットにはactionsなどの一般的なプロパティを含めて、21個のプロパティが存在します。(inherited系統のプロパティは除く)
それらの中でも、よく使われるプロパティを解説していきます。

prop名 説明
currentIndex 今現在アクティブなページ番号を指定します。
onTap タップした時の動作です。ほぼ間違いなくcurrentIndexと関連した処理が書かれます。currentIndexに変数を用意しておき、onTap時にお好きな状態管理手法でcurrentIndexの値を書き換えてページ遷移するという方法がよくあると思います。
type 重要な項目なので後述しますが、BottomNavBarの見た目や動作を決定します。BottomNavigationBarTypeプロパティにfixed,shifting,valuesの3つの値があります。
backgroundColor BottomNavBarの背景色を決定します。
enableFeedback 音/触覚のレスポンスをONにするかどうを決定します。AndroidではONにしてBottomNavBarのアイテムをタップするとカチッという音が鳴り、長押しすると振動が起きます。
iconSize BottomNavBarに設定されている全てのアイコンのサイズを決定します
items このウィジェットのメイン部分ですね、BottomNavBarの実際のページになる部分のアイテムです。BottomNavigationBarItemウィジェットを配置して使用しますが、これについては後述します。
landscapeLayout スマホ画面が横向きの時のBottomNavBarレイアウトを決定します。BottomNavigationBarLandscapeLayoutウィジェットを配置し、spread,centered,linearなどの値から見た目を決定出来ます。基本はnullで問題なくデフォルトのthemeから推測してくれますし、それもnullの場合は、spreadが適応されます。なので意図的に設定する場合はcenteredかlinearが多いです。
selectedFontSize itemが選択された時のそのフォントサイズを決定します。
selectedIconTheme itemが選択された時のそのIconThemeを決定します。※このプロパティを選択した場合、unselectedの方も入力必須となります。
selectedLabelStyle itemが選択された時のTitle(label)のTextStyleを決定します。
selectedItemColor itemが選択された時のそのitemの色を決定します。LabelStyleとIconThemeの色を同時に設定する項目です。
unselectedFontSize 選択されていないitemのフォントサイズを決定します。
unselectedIconTheme 選択されていないitemのIconThemeを決定します。※このプロパティを選択した場合、selectedの方も入力必須となります。
unselectedLabelStyle 選択されていないitemのTitle(label)のTextStyleを決定します。
unselectedItemColor 選択されていないitemの色を決定します。LabelStyleとIconThemeの色を同時に設定する項目です。

色の設定項目が色々あってややこしいですが、基本的にはItemColor系統のプロパティだけで完結できると思います。

豆知識(プロパティの競合)

IconThemeとLabelStyleとItemColorの3つでそれぞれの色を設定した場合は色の競合が起こります。
IconTheme vs ItemColorは IconThemeが勝ちます。
LabelStyle vs ItemColorは ItemColorが勝ちます。

同様に、iconSizeも競合が起こりますが、IconTheme系統で設定した値が勝ちます。

BottomNavigationBarItemについて

itemsプロパティに設定するBottomNavigationBarItemにもいくつかプロパティが存在し、それらも重要なのでここで説明します。

prop名 説明
icon そのitemに表示されるアイコンです。
activeIcon そのitemがアクティブな時に表示されるアイコンです、選択されていない時はunselectedの時はiconでselectedの時はactiveIconという認識で構いません。activeIconがnullの時はiconがselected状態でも表示されます。
label そのitemのタイトル(label)部分です。
tooltip アイコンを長押しした時に表示されるツールチップです。
backgroundColor itemを選択した時に、BottomNavBarの背景色を変更します。itemによってBottomNavBarの色を変更する事が出来るという事です。

ツールチップは入力必須項目ではありませんが、設定しておく事をお勧めします。

BottomNavigationBarのtypeについて

BottomNavigationBarTypeには3つの状態があると言いましたが、アイテムの数によってdefault値が変わります。
アイテムの数が2か3の場合はfixedが適応され、4つ以上の場合はshiftingが適応されます。
もちろんdefault値が変わるだけで、自分でプロパティに値を入れれば4つ以上でもfixedを選択したりなどは出来ます。

fixed(3つの場合)

shifting(4つの場合)

そして、最も重要なのが、BottomNavBarとBottomNavBarItem双方のbackgoundcolorです。

fixedに設定している場合は、Itemの方のbackgroundcolorは機能しません。
shiftingに設定している場合はItemの方のbackgroundcolorが、NavBarのbackgroundcolorに勝ちます。(NavBarを設定していてもしていなくても、Itemのbackgroundcolorを設定している場合は同義ということです)

全部盛りサンプル

デザインはともかくとして、上記に述べたプロパティを出来る限り全て使ったBottomNavigationBarです。

import 'package:flutter/material.dart';

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

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

  static const String _title = 'BottomNavBar Code Sample';

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

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

  
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int _selectedIndex = 0;

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BottomNavigationBar Sample'),
      ),
      body: Center(
        child: Text(
          "indexNum: $_selectedIndex",
          style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.book),
            activeIcon: Icon(Icons.book_online),
            label: 'Book',
            tooltip: "This is a Book Page",
            backgroundColor: Colors.blue,
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            activeIcon: Icon(Icons.business_center),
            label: 'Business',
            tooltip: "This is a Business Page",
            backgroundColor: Colors.green,
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.school),
            activeIcon: Icon(Icons.school_outlined),
            label: 'School',
            tooltip: "This is a School Page",
            backgroundColor: Colors.purple,
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            activeIcon: Icon(Icons.settings_accessibility),
            label: 'Settings',
            tooltip: "This is a Settings Page",
            backgroundColor: Colors.pink,
          ),
        ],

        type: BottomNavigationBarType.shifting,
        // ここで色を設定していても、shiftingにしているので
        // Itemの方のbackgroundColorが勝ちます。
        backgroundColor: Colors.red,
        enableFeedback: true,
        // IconTheme系統の値が優先されます。
        iconSize: 18,
        // 横向きレイアウトは省略します。
        // landscapeLayout: 省略
        selectedFontSize: 20,
        selectedIconTheme: const IconThemeData(size: 30, color: Colors.green),
        selectedLabelStyle: const TextStyle(color: Colors.red),
        // ちなみに、LabelStyleとItemColorの両方を選択した場合、ItemColorが勝ちます。
        selectedItemColor: Colors.black,
        unselectedFontSize: 15,
        unselectedIconTheme: const IconThemeData(size: 25, color: Colors.white),
        unselectedLabelStyle: const TextStyle(color: Colors.purple),
        // IconTheme系統の値が優先されるのでこの値は適応されません。
        unselectedItemColor: Colors.red,
      ),
    );
  }
}

Tips

よくある質問としてまとめます。要望やアイデアなどがあれば、ぜひご連絡ください。

スワイプジェスチャー(横スクロール)で遷移したい。⇒

ScaffoldのbodyにPageViewを指定し、プロパティにコントローラーを登録、そのコントローラーを利用しこの機能を実装出来ます。


final _pageViewController = PageController();


  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        controller: _pageViewController,
        children: <Widget>[
          screenA(title: "A"),
          screenB(title: "B"),
          screenC(title: "C"),
	  screenD(title: "D"),
        ],
        onPageChanged: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
      ),
      BottomNavigationBar(
        currentIndex: _activePage,
        onTap: (index) {
          _pageViewController.animateToPage(index, duration: Duration(milliseconds: 200), curve: Curves.easeOut);
        },
        items: ...
      

Statefulウィジェットじゃなくて他の状態管理手法の方がいいですか?⇒

結論から述べると、時と場合によりますしお好きな方法で構いません。最適解というのは無いと思います。
ただこの結論はつまらないので一応、私の考えをドロップダウン形式で載せます。
発展的内容ですし、状態管理手法は討論を生む可能性がありますので、あくまで私の個人的考えとして受け取ってください。

個人的考え

私は、statefulウィジェットで管理する事が多いです。理由は
・初心者でも読みやすい
・上記のコードで言う_selectedIndexを他のウィジェットで利用する事がほとんど無い
からです。
よく、provider系統で管理した方がコスト安くね?と聞かれるのですが、flutterのbuildライフサイクルは非常に有能で、定数となるウィジェットにconstをきちんと付けていればコストは全く高くありません。

ただ、GetXやRiverpodで実装する時もあります。それは多機能であったり複雑な処理を行うアプリを作成する時です。拡張性のために上記のパッケージを意図的に利用する事もあります。

最初に述べたように、お好きな方法で全く問題ありません。

最後に

ありがとうございました。
またAppBar記事が好評を頂いているようで嬉しい限りです。よければこちらからご覧ください。

https://zenn.dev/urasan/articles/3a4002c00b5026

感想やご要望などはTwitterまで連絡いただければ幸いです。

https://twitter.com/urasan_edu

Discussion

ログインするとコメントできます