[Flutter] BottomNavigationBarを探る: Material、Cupertino、Customの選択肢

2023/12/18に公開

この記事は、株式会社マネーフォワードの福岡開発拠点が主催している Money Forward Fukuoka Advent Calendar 2023 18日目の投稿です。
17日目はtsutsumimi-1209さんのブレイキンのすすめでした。

自己紹介

こんにちは、マネーフォワード福岡拠点でPay事業本部でFrontEnd開発者として勤めているテヒョンソクと申します。皆様にはソクくんと呼ばれております!
最近、趣味で触っているFlutterについて書きたいと思います!

はじめに

Flutterでのタブナビゲーションを実装するのは、一つの料理を作るだけでもさまざまな方法や材料があることに似て、選択肢が多いです!伝統的なマテリアルデザインから洗練されたクパチーノスタイル、自身のカスタムタブまで、スタイルに限りがありません!この記事では、マテリアルデザインversion2, 3, クパチーノ, カスタムといったスタイルを簡単に比較します。目的は、各スタイルのパフォーマンスを詳細に分析することではなく、それぞれがどのように異なるかを理解し、プロジェクトに最適なものを見つけることです。どのスタイルがニーズに最も適しているかを一緒に探し、より洗練されたUXを提供するアプリケーションを作りたいです!正解はない私の個人的な意見がたくさんあります!ぜひ皆様の意見を聞かせてください!

BottomNavigationBar(Material 2)

BottomNavigationBarはとても面白いです!Material 2スタイルのBottomNavigationBarを簡単に実装してみましょう。以下は、基本的なホームと検索の2つのタブを持つサンプルコードです。

class _BottomNavigationBarUiState extends State<BottomNavigationBarUi> {
  int _selectedIndex = 0;

  final _screens = [
    // Home screen
    const Center(
      child: Text(
        'HOME',
        style: TextStyle(fontSize: FontSizes.xxl),
      ),
    ),
    // Search screen
    const Center(
      child: Text(
        'SEARCH',
        style: TextStyle(fontSize: FontSizes.xxl),
      ),
    )
  ];

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: _screens[_selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: _onTab,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
        ],
      ),
    );
  }
}


この例では、FlutterのBottomNavigationBarを使用してシンプルなタブナビゲーションを実装しています。この実装には、'ホーム'と'検索'の2つのタブがあり、ユーザーがタブを選択すると該当画面に切り替わります。デザインがとてもシンプルで個人的にはとても好きです。コードを簡単に説明すると、

  • _onTab: タブが選択されると呼ばれる関数で、選択されたタブのインデックスを_selectedIndexに設定します
  • body: 現在選択されたタブに対応する画面を表示します
  • bottomNavigationBar: BottomNavigationBarウィジェットを使用し、タブを構成します- -- currentIndexは現在選択されたタブのインデックスを示し、onTapはタブ選択時に呼ばれる関数です。

BottomNavigationBarの最も面白いのはここからです!!
ここから、アイテムを4つ以上追加し、

  final _screens = [
   // Home screen
    const Center(
      child: Text(
        'HOME',
        style: TextStyle(fontSize: FontSizes.xxl),
      ),
    ),
   ... + 3Screens code
  ];

それぞれに背景色を設定すると!

      items: const [
          BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home',
              backgroundColor: Colors.green),
          BottomNavigationBarItem(
              icon: Icon(Icons.search),
              label: 'Search',
              backgroundColor: Colors.red),
		...rest
        ],


面白くないでしょうか?私はとても不思議で驚きました!笑
デザインに自信がなくても、より美しい効果を生み出せます
この変化の理由は、アイテムが4つ以上になると、デフォルトのtype: BottomNavigationBarType.fixedから自動的にtype: BottomNavigationBarType.shiftingに変わるためです!
これ以外にもBottomNavigationBarにはたくさんの面白いOptionsがあるので!ぜひ遊んでみてください。

https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html

次はFlutterでMaterial3スタイルのNavigationBarです!ここで最も魅力的なのは「バッジ(Badge)」Widgetの活用です!これもとても面白いです!

Sample code

class _NavigationBarUiState extends State<NavigationBarUi> {
  int _selectedIndex = 0;

  final _screens = [
  const Center(
      child: Text(
        'HOME',
        style: TextStyle(fontSize: FontSizes.xxl),
      ),
    ),
   ...rest
  ];

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: _screens[_selectedIndex],
      bottomNavigationBar: NavigationBar(
          onDestinationSelected: _onTab,
          selectedIndex: _selectedIndex,
          destinations: const [
            NavigationDestination(
              icon: Icon(Icons.home),
              label: 'Home',
            ),
		  ...
            NavigationDestination(
              icon: Badge(
                label: Text('2'),
                child: Icon(Icons.message),
              ),
              label: 'Search',
            ),
          ]),
    );
  }
}


個人的にはDestinationというもっとわかりやすい名前で、可読性が高くなっているのではないかなとおもいました!そしてここで最も良いのは

 NavigationDestination(
              icon: Badge(
                label: Text('2'),
                child: Icon(Icons.message),
              ),
              label: 'Search',
            ),

ここです!!他のWidgetはStackというWidgetで表現しなければならないものをここではBadgeひとつで実装できます!!とても面白いです!!
個人的な意見を片付けると、

  • Design => BottomNavigationBarのデザインが好きですが、NavigationBarはバッジのような特定の機能をより簡単に実装できる利点があります
  • メッセージや通知が含まれるタブナビゲーションを作る場合、BottomNavigationBarよりもNavigationBarを使う方が良い選択になります。特に、Stackウィジェットを使用せずにバッジ機能を簡単に実装できるので時間の節約ができます!

CupertinoTabBar

FlutterでiOSスタイルを実現したい場合、良い選択になるかもしれません!これはiOSの下部タブナビゲーションを効果的に再現しますが、個人的にはCupertinoTabBarはお勧めしたくありません。一旦コードから見てみましょう!

Sample code

class _CupertinoTabUiState extends State<CupertinoTabUi> {
  final screens = [
    const Center(child: Text('Home', style: TextStyle(fontSize: FontSizes.xxl))),
    const Center(child: Text('Search', style: TextStyle(fontSize: FontSizes.xxl))),
  ];

  
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(items: const [
        BottomNavigationBarItem(icon: Icon(CupertinoIcons.house), label: 'Home'),
        BottomNavigationBarItem(icon: Icon(CupertinoIcons.search), label: "Search"),
      ]),
      tabBuilder: (context, index) => screens[index],
    );
  }
}


CupertinoTabBarは内部的にタブの切り替えを処理するため、別途onTabのようなハンドラーを設定する必要がなく、コードが非常に簡潔です。そして、iPhoneユーザーの私にはとても親しみがあります!笑
しかし、私がCupertinoTabBarをあまり好まない理由はいくつかあります。

  • MaterialAppとの統合問題:CupertinoTabBarをMaterialAppと一緒に使うと、スタイリングや機能面で問題が生じることがあります。例えば、上の画面が崩れるのも、main.dartでMaterialAppでラップしているためです。
  • カスタマイズの難しさ:カスタマイズには制限が多く、自由度が低いです。
    もしiOSのデザインを利用したい場合にはありよりのありかもしれませんが、もっと独特なデザインやAnimationを加えたい場合、私は他のオプションを選ぶと思いました!!しかし、これはあくまで個人的な意見です!

https://api.flutter.dev/flutter/cupertino/CupertinoTabBar-class.html

CustomNavbar

Flutterでは、標準的なナビゲーションバーよりも一歩進んだユーザーエクスペリエンスを提供するために、カスタムナビゲーションバーの実装もできます!たくさんカスタムしてはいませんが簡単に紹介をしたいと思います!

SampleCode

nav_item.dart

class NavItem extends StatelessWidget {
  const NavItem(
      {super.key,
      required this.icon,
      required this.text,
      required this.isSelected,
      required this.onTap,
      required this.selectedIcon});
  final IconData icon;
  final IconData selectedIcon;
  final String text;
  final bool isSelected;
  final Function onTap;

  
  Widget build(BuildContext context) {
    return Expanded(
      child: GestureDetector(
        onTap: () => onTap(),
        child: Container(
          color: Colors.black,
          child: AnimatedOpacity(
            opacity: isSelected ? 1 : 0.6,
            duration: const Duration(milliseconds: 100),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                FaIcon(
                  isSelected ? selectedIcon : icon,
                  color: Colors.white,
                ),
                const SizedBox(
                  height: 5.0,
                ),
                Text(
                  text,
                  style: const TextStyle(
                      color: Colors.white, fontSize: FontSizes.xs),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

NavItemは、アイコン、テキスト、選択状態(isSelected)、タップイベント処理(onTap)など、必要な要素をパラメータとして受け取り、再利用が可能なウィジェットを作りました!ここで、IconがSelectedされたときのとても簡単なAnimationや色変化を追加しました!このアニメーションは、選択されたタブが視覚的に目立つようにするための重要な要素です。例えば、選択されたアイコンはより明るく表示され、ユーザーが直感的に現在の位置を理解できるようにします。
custom_nav_ui.dart

Widget build(BuildContext context) {
    return Scaffold(
      body: _screens.elementAt(_selectedIndex),
      bottomNavigationBar: BottomAppBar(
        color: Colors.black,
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child:
              Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
            NavItem(
                icon: FontAwesomeIcons.circle,
                selectedIcon: FontAwesomeIcons.solidCircle,
                text: "Home",
                isSelected: _selectedIndex == 0,
                onTap: () => onTap(0)),
            NavItem(
                icon: FontAwesomeIcons.message,
                selectedIcon: FontAwesomeIcons.solidMessage,
                text: "Message",
                isSelected: _selectedIndex == 1,
                onTap: () => onTap(1)),
		 ...rest
          ]),
        ),
      ),
    );
  }
}

ここでimportし、使用をしております!せっかくCustomなのでFontAwesomeIconで作ってみました!クリックしたときSolidIconの変化を見せたかったのですが、課金をしていないのであるものを適当に入れたためHomeが丸になってしまいました笑

とても簡単にカスタムしたので綺麗ではないかもしれませんが、自分が入れたいアニメーション効果を全て入れることができます!デザインに自信がないので説得力は足りないのですが個人的にはとても良い選択肢ではないかなと思いました!

標準のウィジェットにはない、創造的な機能とスタイルを実現することが可能なのでFlutterでのユニークなUIを作成してみてください!

最後に

今回はFlutterのナビゲーションオプションを一通り見てきましたが、いかがでしょうか?Flutterの最大の魅力は、豊富な選択肢の中から自分に合ったものを選べることです!Material、Cupertino、そしてカスタムナビゲーションバーと、それぞれに独自の特徴があり、プロジェクトのニーズに合わせて最適なものを選択することができます。

Flutterでの開発は、多くの選択肢を探る創造的な旅のようです!とても面白いです!今回の記事を通して私の個人的な意見を書きました!これはあくまでも一つの観点ではありますが、同じことで迷っている方の一助になれば幸いです。
初めてテックブログを書いてみたので、まだ慣れていないところが多いかもしれませんが、皆さんのご意見やフィードバックをぜひお聞かせください!!ありがとうございました!

Discussion