リバース・エンジニアリング

Flultterとテックブログと時々iOS

15. Flutter のBottomNavigationBarでUITabbarControllerぽく作る

今回はFlutterでBottomNavigatonBarの拡張を行いたいと思います。 またBottomNavigatonBarの基本的な使い方は以前に学習しました。

blog.tamappe.com

こちらは画面の切り替えができておらず、本当にアイコンの選択/未選択とウィジェットの更新だけの実装でした。 そこで今回はアイコンだけでなく、画面切り替えの部分も学習していきます。 ルーティングを使いますので、ルーティングについて学習します。

ルーティングとは

ルーティングとは画面遷移をpathで表現する方法です。 StatelessWidgetにはルーティングを設定するroutesというプロパティが存在します。 さらにアプリ起動時にどのウィジェットを表示させるかを指定するためのinitialRouteのプロパティも存在します。

使い方としては次のような書き方です。

MaterialApp(
      title: [タイトル],
      initialRoute: '/',
      routes: {
        '/': (context) => MainPage(), /// MainPageウィジェットに飛ばす
      },
    );

これをもとにしてBottomNavigationBarのアイコンをタップしたときの画面遷移を実装しようと思ったのですが 使い方が間違っていたみたいです。 前日ですが、無限ループが発生してアプリが起動できませんでした。

今回は2回目として別の方法で実装しました。

アプローチの方法

イメージとしては次の画像のようになります。

f:id:qed805:20200205221733p:plain
BottomNavigationBarのイメージ

メインとなるウィジェット(今回のサンプルコードではMainPage と命名しました)を用意します。 メインウィジェットでScaffoldのbodyにIndexedStackを使います。 これでindexに応じたウィジェットのStackが出来上がります。 あとはBottomNavigationBarのonTapcurrentIndexにて選択されたindexを再代入します。

これで動的にBottomNavigationBarのアイコンをタップしたことで画面が変わったかのような実装を実現することができます。

サンプルコード

では早速、このアプローチをもとにしたサンプルコードを書いていきます。

今回の登場人物は

  • main.dart
  • main_page.dart
  • first_screen
  • second_screen
  • third_screen

の5つのクラスです。

main.dart

import 'package:flutter/material.dart';
import 'package:practice_app/main_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final title = 'BottomNavigationBar複数画面のサンプルコード';
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: true,
      title: 'BottomNavigationBar App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
        primaryColor: const Color(0xff2196f3),
        accentColor: const Color(0xff2196f3),
        canvasColor: const Color(0xfffafafa),
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => MainPage(),
      },
    );
  }
}

main_page.dart

import 'package:flutter/material.dart';
import 'package:practice_app/first_screen.dart';
import 'package:practice_app/second_screen.dart';
import 'package:practice_app/third_screen.dart';

class MainPage extends StatefulWidget {
  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _selectedIndex = 0;
  List<Widget> _pageList = [
    FirstScreen(),
    SecondScreen(),
    ThirdScreen()
  ];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: _pageList,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
            title: Text('First'),
            icon: Icon(Icons.home)
          ),
          BottomNavigationBarItem(
              title: Text('Second'),
              icon: Icon(Icons.favorite)
          ),
          BottomNavigationBarItem(
              title: Text('Third'),
              icon: Icon(Icons.android)
          ),
        ] ,
        selectedItemColor: Colors.red,
        currentIndex: _selectedIndex,
        onTap: (int index) {
          setState(() {
            _selectedIndex = index;
          });
        },
      ),
    );
  }
}

first_screen.dart

import 'package:flutter/material.dart';

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('First Screen'),
    );
  }
}

second_screen.dart

import 'package:flutter/material.dart';

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Second Screen'),
    );
  }
}

third_screen.dart

import 'package:flutter/material.dart';

class ThirdScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('ThirdScreen'),
    );
  }
}

となります。 これでアプリを実行すると次のような画面が表示されます。

first second third
f:id:qed805:20200205222847p:plain
first
f:id:qed805:20200205222859p:plain
second
f:id:qed805:20200205222912p:plain
third

このように表示されたら成功です。 iOS のUITabbarController みたいな画面切り替えが実装できました。 このような動きが実現できれば簡単なアプリが作れそうですね。

注記

最後にBottomNavigationBarの注記になります。 現在のFlutterの不具合なのか、iOSではBottomNavigationBarItemにおけるアイコンは3つまでで4つ以上置こうとすると挙動がおかしくなり 最初の1つ目のアイコンしか表示されません。 では、4つ目のアイコンがおけないのかというと置けます

下記の参考ページに解決法が載っています。

github.com

つまりはBottomNavigationBarにtypeというプロパティがあり、それにBottomNavigationBarType.fixedをセットすればいいです。

BottomNavigationBar(
        type: BottomNavigationBarType.fixed,  /// これを追加する
        items: [
          BottomNavigationBarItem(
            title: Text('First'),
            icon: Icon(Icons.home)
          ),
          BottomNavigationBarItem(
              title: Text('Second'),
              icon: Icon(Icons.favorite)
          ),
          BottomNavigationBarItem(
              title: Text('Third'),
              icon: Icon(Icons.android)
          ),
          BottomNavigationBarItem(
              title: Text('Third'),
              icon: Icon(Icons.settings)
          ),
        ]

これで4つ以上おけるようになります。 以上、注記でした。