Chapter 06

Step5: ボトムナビゲーションで設定画面をつくろう

sugit
sugit
2023.05.07に更新

Step5 の概要

Step5 では、ボトムナビゲーションバーを追加し、メインの画面と設定画面を切り替えられるようにします。

本章で学べること

  • BottomNavigationBar の使い方
  • BottomNavigation で画面を切り替える方法

ボトムナビゲーションを作る

では、設定画面を作ってみましょう。設定はボトムナビゲーションにあったり、AppBar からドロワーが出てきてその中にあったり、はたまた FAB (Floating Action Button) にあったりとさまざまです。

ここでは一番ベーシックな(?)ボトムナビゲーションを使ってみようと思います。

とりあえず home と settings の 2 つにします。

class TopPage extends StatelessWidget {
  const TopPage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ListView.builder(
          padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
          itemCount: 1010,
          itemBuilder: (context, index) => PokeListItem(index: index),
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (index) => {},
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            label: 'home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'settings',
          ),
        ],
      ),
    );
  }
}

この状態でボトムナビゲーションをクリックしても何もおきません。ボトムナビゲーションをつかうためには、

  • タップしたらそっちをフォーカス
  • フォーカス位置にあった画面を body に表示

をする必要があります。

ということは、いまどちらがタップされているのかを記憶しておく必要があります。

ここではじめて 状態 を持った画面が必要になりました。

ここまでは完全にただのお絵かきでしたが、アプリは状態を保つことでインタラクティブになります。これによってちょっとずつアプリっぽくなってきますね。

状態の持ち方にはたくさんの方法があります。状態管理と呼ばれるものです。状態管理ライブラリの選定にはさまざまな選択肢がありますが、そういうリッチなものを使わなくてもある程度のことはできます。

Flutter における状態管理のもっとも基本となるのが StatefulWidget です。

StatefulWidget は Widget と State のペアで構成されます。この辺りは Widget ツリーのことを深く知ってくるとわかるのですが、後回しにしましょう。

StatelessWidget を StatefulWidget に書き換えるところはとりあえず気合で覚えてください。

class TopPage extends StatefulWidget {
  const TopPage({Key? key}) : super(key: key);
  
  _TopPageState createState() => _TopPageState();
}

class _TopPageState extends State<TopPage> {
  
  Widget build(BuildContext context) {
    return SomethingNiceWidget(....);
  }
}
  • StatefulWidget は State とペア
  • createState で StatefulWidget に State を紐づける
  • State 側は単純に続きの Widget を返す

で OK です。

StatefulWidget - State の関係については 別冊に記載(予定)の Widget ツリーの説明をご覧ください。

ボトムナビゲーションのアクディブなメニューを状態として持たせてみます。状態の更新は setState() で実施します。

class TopPage extends StatefulWidget {
  const TopPage({Key? key}) : super(key: key);
  
  _TopPageState createState() => _TopPageState();
}

class _TopPageState extends State<TopPage> {
  int currentbnb = 0;
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ListView.builder(
          padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
          itemCount: 1010,
          itemBuilder: (context, index) => PokeListItem(index: index),
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (index) => {
          setState(
            () => currentbnb = index,
          )
        },
        currentIndex: currentbnb,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            label: 'home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'settings',
          ),
        ],
      ),
    );
  }
}

注意点として、setState にはコールバック形式で値の更新を与えます。

これは、Widget はあくまで設計図であり、実行時に決まる値は関数で与えるべしというものなのですが、この辺りも Build サイクルを知ってからですね。Build のことが気になって仕方がない方は別冊をご覧ください。

ボトムナビゲーションを使ってメイン画面を切り替える

なにはともあれ、これでボトムナビゲーションの選択状態は切り替わるようになりました。

これに合わせて、body のメイン画面を切り替えます。

切り替えやすいように ListView を独立した Widget に取り出し、PokeList と名付けておきましょう。

class PokeList extends StatelessWidget {
  const PokeList({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
      itemCount: 1010,
      itemBuilder: (context, index) => PokeListItem(index: index),
    );
  }
}

同じように Settings を作ります。

class Settings extends StatelessWidget {
  const Settings({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return ListView(
      children: const [
        ListTile(
          leading: Icon(Icons.lightbulb),
          title: Text('Dark/Light Mode'),
        ),
      ],
    );
  }
}

これを PokeList の代わりに body に入れるとこうなります。

あとはこれを切り替えましょう。

body: SafeArea(
        child: currentbnb == 0 ? const PokeList() : const Settings(),
      ),

これで OK です。
実は、この実装では Widget をがっつり切り替えているので、切り替え時にツリーの情報が失われます。
これについては以下の記事の通り解決方法はありますが、ここでは手を触れずに進めます。Step10 でカイゼンします。

https://stackoverflow.com/questions/49439047/how-to-preserve-widget-states-in-flutter-when-navigating-using-bottomnavigation