【Flutter】 iphoneのようにボトムナビゲーションを作る

Flutterでの下タブでのナビゲーションについてです。

マテリアルデザインであればそのままBottomNavigationBarを使用すればいいのですがその場合
画面遷移で下タブが消えてしまうので遷移後に別タブの画面切り替えができません。

今回の記事はiosのように下タブを残したまま画面遷移をさせる方法についてです。
以下を記述しています。

- CupertinoTabScaffoldを使用しながら下タブを表示させずに画面遷移する方法
- androidのバックキーを押下した際に現在のタブから前画面に戻る方法



##CupertinoTabScaffoldを使用しながら下タブを表示させずに画面遷移する方法
画面遷移する際にrootNavigatorをtrueにしてpushします。

```dart:
Navigator.of(context, rootNavigator: true).push(
   MaterialPageRoute(
      builder: (context) => nextPage(),
   ),
);
```


##androidのバックキー押下した際に現在のタブから前画面に戻る方法
上記のpushのように下タブを表示せずに画面遷移した場合は問題ありませんが
CupertinoTabScaffoldを使用して下タブを表示しながら画面遷移をした場合androidでバックキーを押すとアプリが閉じます。
これはバックキーでpopの処理が行われているのですが
表示されている画面のcontextではなく、CupertinoTabScaffoldを使用した親画面のcontextを用いて
popしているため親画面は画面遷移していないのでアプリが閉じています。
※iOSの場合は特に何もしなくても画面を左スワイプで前画面に戻ることが可能です。

方法としてはstaticで各タブのcontextを保持しておき、バックキーを押されたタイミングで親画面から各タブのcontextを呼び出しpopします。

```dart:
/// androidのバックキー制御するための各タブ分のcontext保持クラス
class ConstantsChildContext {
  static int selectedIndex = 0;
  static BuildContext childContext0;
  static BuildContext childContext1;
  static BuildContext childContext2;
}
```

各タブのbuild時にcontextを初回だけセットします。

```dart:
/// 初回に各タブのChildContextをセットする
void setChildContext({@required BuildContext childContext}) {
  switch (ConstantsChildContext.selectedIndex) {
    case 0:
      if (ConstantsChildContext.childContext0 == null) {
        ConstantsChildContext.childContext0 = childContext;
      }
      break;
    case 1:
      if (ConstantsChildContext.childContext1 == null) {
        ConstantsChildContext.childContext1 = childContext;
      }
      break;
    case 2:
      if (ConstantsChildContext.childContext2 == null) {
        ConstantsChildContext.childContext2 = childContext;
      }
      break;
  }
}
```

最後に親画面でCupertinoTabScaffoldの親WidgetとしてWillPopScopeを書き、
ここでバックキーを押した際のイベントを取得します。

```dart:
     @override
  Widget build(BuildContext context) {
    /// WillPopScopeで親画面のbackKeyイベントを取得し、現在のタブのcontextをpopして前画面に戻る
    return WillPopScope(
      onWillPop: () async {
        /// バックキー押下時のイベント取得
        return _onBackKeyAndroid();
      },
      child: CupertinoTabScaffold(),
    );
  }
```

各タブで前画面に戻れるかどうかを判定しbool値をかえします。

```dart:
 /// バックキーをタップすると各画面のpopを実行(Androidのみ)
  bool _onBackKeyAndroid() {
    if (Platform.isAndroid) {
      switch (ConstantsChildContext.selectedIndex) {
        case 0:
          if (Navigator.canPop(ConstantsChildContext.childContext0)) {
            Navigator.pop(ConstantsChildContext.childContext0);
          } else {
            /// 前画面に戻れない場合にアプリを閉じたくなければここはfalse
            return true;
          }
          break;
        case 1:
          if (Navigator.canPop(ConstantsChildContext.childContext1)) {
            Navigator.pop(ConstantsChildContext.childContext1);
          } else {
            return true;
          }
          break;
        case 2:
          if (Navigator.canPop(ConstantsChildContext.childContext2)) {
            Navigator.pop(ConstantsChildContext.childContext2);
          } else {
            return true;
          }
          break;
      }
    }
    return false;
  }
```

これでandroidのバックキー制御ができるようになりました。

[こちら](https://dev.classmethod.jp/articles/basic_bottom_navigation_flutter/)で勉強しました。
サンプルコードのデザインはそのままです。

### サンプルコード

```dart:main.dart
import 'package:flutter/material.dart';

import 'home_page.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomePage(),
    );
  }
}

```

```dart:home_page.dart
import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import 'constants_child_context.dart';
import 'custom_page.dart';

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  static List<Widget> _pageList = [
    CustomPage(
      pannelColor: Colors.cyan,
      title: 'Home',
      pageCount: 1,
    ),
    CustomPage(
      pannelColor: Colors.green,
      title: 'Settings',
      pageCount: 1,
    ),
    CustomPage(
      pannelColor: Colors.pink,
      title: 'Search',
      pageCount: 1,
    )
  ];

  void _onItemTapped(int index) {
    setState(() {
      ConstantsChildContext.selectedIndex = index;
    });
  }

  /// バックキーをタップすると各画面のpopを実行(Androidのみ)
  bool _onBackKeyAndroid() {
    if (Platform.isAndroid) {
      switch (ConstantsChildContext.selectedIndex) {
        case 0:
          if (Navigator.canPop(ConstantsChildContext.childContext0)) {
            Navigator.pop(ConstantsChildContext.childContext0);
          } else {
            /// 前画面に戻れない場合にアプリを閉じたくなければここはfalse
            return true;
          }
          break;
        case 1:
          if (Navigator.canPop(ConstantsChildContext.childContext1)) {
            Navigator.pop(ConstantsChildContext.childContext1);
          } else {
            return true;
          }
          break;
        case 2:
          if (Navigator.canPop(ConstantsChildContext.childContext2)) {
            Navigator.pop(ConstantsChildContext.childContext2);
          } else {
            return true;
          }
          break;
      }
    }
    return false;
  }

  @override
  Widget build(BuildContext context) {
    /// WillPopScopeで親画面のbackKeyイベントを取得し、現在のタブのcontextをpopして前画面に戻る
    return WillPopScope(
      onWillPop: () async {
        return _onBackKeyAndroid();
      },
      child: CupertinoTabScaffold(
        tabBar: CupertinoTabBar(
          items: [
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.settings),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.search),
            ),
          ],
          currentIndex: ConstantsChildContext.selectedIndex,
          onTap: _onItemTapped,
          backgroundColor: Colors.white,
        ),
        tabBuilder: (context, index) {
          return CupertinoTabView(
            builder: (context) {
              return _pageList[index];
            },
          );
        },
      ),
    );
  }
}

```

```dart:constants_child_context.dart
import 'package:flutter/cupertino.dart';

/// androidのバックキー制御するための各タブ分のcontext保持クラス
class ConstantsChildContext {
  static int selectedIndex = 0;
  static BuildContext childContext0;
  static BuildContext childContext1;
  static BuildContext childContext2;
}

/// 初回に各タブのChildContextをセットする
void setChildContext({@required BuildContext childContext}) {
  switch (ConstantsChildContext.selectedIndex) {
    case 0:
      if (ConstantsChildContext.childContext0 == null) {
        ConstantsChildContext.childContext0 = childContext;
      }
      break;
    case 1:
      if (ConstantsChildContext.childContext1 == null) {
        ConstantsChildContext.childContext1 = childContext;
      }
      break;
    case 2:
      if (ConstantsChildContext.childContext2 == null) {
        ConstantsChildContext.childContext2 = childContext;
      }
      break;
  }
}

```

```dart:custom_page.dart
import 'package:flutter/material.dart';

import 'constants_child_context.dart';
import 'full_screen_custom_page.dart';

class CustomPage extends StatelessWidget {
  final Color pannelColor;
  final String title;
  final int pageCount;

  CustomPage(
      {@required this.pannelColor,
      @required this.title,
      @required this.pageCount});

  @override
  Widget build(BuildContext context) {
    setChildContext(childContext: context);
    final titleTextStyle = Theme.of(context).textTheme.title;
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 200,
                height: 200,
                decoration: BoxDecoration(
                    color: pannelColor,
                    borderRadius: BorderRadius.all(Radius.circular(20.0))),
                child: Center(
                  child: Text(
                    title + pageCount.toString(),
                    style: TextStyle(
                      fontSize: titleTextStyle.fontSize,
                      color: titleTextStyle.color,
                    ),
                  ),
                ),
              ),
              TextButton(
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => CustomPage(
                          pannelColor: pannelColor,
                          title: title,
                          pageCount: pageCount + 1,
                        ),
                      ),
                    );
                  },
                  child: Text('下タブあり次画面')),
              TextButton(
                  onPressed: () {
                    Navigator.of(context, rootNavigator: true).push(
                      MaterialPageRoute(
                        builder: (context) => FullScreenCustomPage(
                            pannelColor: pannelColor,
                            title: title,
                            pageCount: pageCount + 1),
                      ),
                    );
                  },
                  child: Text('下タブなし次画面')),
            ],
          ),
        ),
      ),
    );
  }
}

```

```dart:full_screen_custom_page.dart
import 'package:flutter/material.dart';

class FullScreenCustomPage extends StatelessWidget {
  final Color pannelColor;
  final String title;
  final int pageCount;

  FullScreenCustomPage(
      {@required this.pannelColor,
      @required this.title,
      @required this.pageCount});

  @override
  Widget build(BuildContext context) {
    final titleTextStyle = Theme.of(context).textTheme.title;
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 200,
                height: 200,
                decoration: BoxDecoration(
                    color: pannelColor,
                    borderRadius: BorderRadius.all(Radius.circular(20.0))),
                child: Center(
                  child: Text(
                    title + pageCount.toString(),
                    style: TextStyle(
                      fontSize: titleTextStyle.fontSize,
                      color: titleTextStyle.color,
                    ),
                  ),
                ),
              ),
              TextButton(
                  onPressed: () {
                    Navigator.of(context, rootNavigator: true).push(
                      MaterialPageRoute(
                        builder: (context) => FullScreenCustomPage(
                          pannelColor: pannelColor,
                          title: title,
                          pageCount: pageCount + 1,
                        ),
                      ),
                    );
                  },
                  child: Text('次画面へ')),
            ],
          ),
        ),
      ),
    );
  }
}

```