Help us understand the problem. What is going on with this article?

flutterのBottom Navigationで苦しんだ話 その1

More than 1 year has passed since last update.

flutterの勉強をしていて、Bottom Navigationについて相当苦しんだので共有しようと思います。

スクリーンショット 2018-12-03 22.09.20.png

Bottom Navigationといえば上の画像にもあるようにいわゆる「下タブ」です。
instagramやfacebookなど、多くのアプリがこの形式をとっており、自分自身でアプリを作ろうと思った際にもとりあえずBottom Navigationで、ということも少なくないのではないでしょうか。ハンバーガーボタンと比較して画面遷移がシームレスだったりモーダル画面作らなくてよかったり、UI的にも開発的にも優しいように見えます。

当然、flutterもこの辺のBottom Navigationはデフォルトで備えています。では何がそんなにハマるのでしょうか。

パターン1: pageControllerで頑張る

1つ目の方法としては、Bottom Navigationを呼び出して、PageControllerを使ってページの操作を行うものです。

まずサンプルのページを用意します。これはほぼインストール時そのままのページを使ってます(実際なんでもOKです)

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}

このページを呼び出すようにBottom Navigationを作成します。

class BasePage extends StatefulWidget {
  @override
  _BasePageState createState() => _BasePageState();
}

class _BasePageState extends State<BasePage> {

  // page index
  int _index = 0;
  PageController _pageController;

  @override
  void initState() {
    super.initState();
    _pageController = new PageController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My Flutter App'),
      ),
      body: new PageView(
          controller: _pageController,
          onPageChanged: (int index) {
            setState(() {
              this._index = index;
            });
          },
          children: [
            MyHomePage(title: "page 1"), //call pages
            MyHomePage(title: "page 2"),
            MyHomePage(title: "page 3"),
          ]),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (int index) { // define animation
          _pageController.animateToPage(index,
              duration: const Duration(milliseconds: 10), curve: Curves.ease);
        },
        currentIndex: _index,
        items: [
          BottomNavigationBarItem( // call each bottom item
            icon: new Icon(Icons.home),
            title: new Text('Home'),
          ),
          BottomNavigationBarItem(
            icon: new Icon(Icons.mail),
            title: new Text('Messages'),
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.person), title: Text('Profile'))
        ],
      ),
    );
  }
}

どうでしょう。pageControllerを呼び出すことでページの遷移を定義すればこのように簡単に
Bottom Navigationを利用できます。しかもページ間スワイプで移動もできます。

この書き方の長所としては、作成が簡単で、ページ間をスワイプ移動できるメリットがあります。しかし一方でページを遷移するたびにいちいち初期化するために、値を保持したり裏で計算を別のページで回すといったことができません(サンプルだと+ボタンで数字をインクリメントしてもページ移動するとリセットされることが分かります)

さて、どうしましょう

パターン2: Stackで重ねる

さて、パターン1では毎回ページが破棄されることが問題でした。ではページを破棄されないように持っておけばいいじゃないかというのが2つ目の考え方になります。

class _BasePageState extends State<BasePage> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My Flutter App'),
      ),
      body: new Stack(
        children: <Widget>[
          _buildOffstage(0, MyHomePage(title: "page 1")),
          _buildOffstage(1, MyHomePage(title: "page 2")),
          _buildOffstage(2, MyHomePage(title: "page 3")),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (int index) {
          setState(() {
            this._index = index;
          });
        },
        currentIndex: _index,
        items: [
          BottomNavigationBarItem(
            icon: new Icon(Icons.home),
            title: new Text('Home'),
          ),
          BottomNavigationBarItem(
            icon: new Icon(Icons.mail),
            title: new Text('Messages'),
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.person), title: Text('Profile'))
        ],
      ),
    );
  }

  // onstage view is only visible 
  Widget _buildOffstage(int index, Widget page) {
    return Offstage(
      offstage: index != _index,
      child: new TickerMode(
        enabled: index == _index,
        child: page,
      ),
    );
  }
}

上のコードではpageControllerを使わなくなった代わりにOffStageを使い、選択されているView以外は非表示にする処理を加えました。これにより、タップして下画面のページを切り替えてもページが消去されないため、インクリメントした値が引き継がれるのが分かるかと思います。

この書き方をするとページ間をスワイプ移動できなくなります。しかし、ページの間を移っても状態が保持されるのでページをまたいで値を保持したい場合には便利です。

パターン2の問題点

さて、パターン2だと何が問題になるでしょうか。ページ遷移を行った時にBottom Navigationが消えてしまうという問題が発生します。じゃあ遷移先にBottom Navigationつければいいじゃん!!ってなるかもしれませんがそうはうまくいきません。アニメーションを入れるとBottom Navigationごとスワイプしてしまいます。

これらの話は長くなりそうなので次回に回せればと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away