【Flutter】BottomNavigationBarを本気で学ぶ
この記事について
本気で学ぶシリーズ第2弾です。公式ドキュメントを参考に、初学者から中級者に向けてわかりやすく解説出来ればと思います。指摘事項や助言などは歓迎です。もしございましたら、Twitterもしくは記事コメントまでお願い致します。
Material Design3のNavigationBarについて。
新しい記事を書きました。Material Design3のNavigationBarについて学びたい方はこちらをご覧ください。
BottomNavigationBarの基礎
LINEやTwitterなど、最近のアプリケーションデザインはTopNavBar(TabBar)ではなくBottomNavbarを導入している所が多いですね。FlutterでもBottomNavBarの書き方を学んでいきましょう。
BottomNavigationBarクラスはScaffoldのbottomNavigationBarプロパティに配置します。
そして、BottomNavigationBarは以下の必須条件が含まれます。
・ページ(NavItems)が二つ以上含まれている
・それらのページのアイコンはnullには出来ない
・それらのページのTitle(label)はnullには出来ない
また、FlutterのBottomNavigationBarはMaterialDesignを元に構成されています。詳しいデザインなどは以下の公式HPを参考にしてください。
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で実装する時もあります。それは多機能であったり複雑な処理を行うアプリを作成する時です。拡張性のために上記のパッケージを意図的に利用する事もあります。
最初に述べたように、お好きな方法で全く問題ありません。
追記(2023/6/22):
GoRouterが更新されました。
今後はNavigationBarを使用する際はGoRouterパッケージを使うと、State管理が後々楽かもしれません。
最後に
ありがとうございました。
またAppBar記事が好評を頂いているようで嬉しい限りです。よければこちらからご覧ください。
感想やご要望などはTwitterまで連絡いただければ幸いです。
Discussion
現在はMaterial3版のNavigationBarがあるようですね