flutter で Stateless な BottomNavigationBar を作る記事がよかったのでやってみた

8 min読了の目安(約7900字TECH技術記事

久しぶりに flutter 触ったら色々忘れていたので復習も兼ねて。
環境は Intel Mac (Big Sur 11.1) です。

TL;DR

CLI でプロジェクト作成

ターミナルで

flutter create myapp
cd myapp
# iOSシミュレーターが起動してなければ起動させる
open -a Simulator
# コンパイルして実行。flutter create しただけの状態でも可
flutter run

出来上がりのイメージ

最終的なファイル一覧

必要なパッケージを入れる

今回は追加で導入するパッケージは2つです。

pubspec.yaml をこんな感じにします。
書き換えたあと flutter pub get するのが正式な方法ですが、
VSCode に flutter 拡張機能入れてあればファイルを編集した時点で自動で flutter pub get してくれます。

pubspec.yaml
name: myApp
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.0
  provider: ^4.3.2+3

dev_dependencies:
  pedantic_mono: any
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

pedantic_mono 用の analysis_options.yaml を用意する

ルートディレクトリに analysis_options.yaml を用意しておきます。
これで lint が効くようになります。

touch analysis_options.yaml
analysis_options.yaml
include: package:pedantic_mono/analysis_options.yaml

フッター切り替え時の画面を作る

ここではアイコンを中央に配置してるだけの画面を作ります。

mkdir -p lib/views/screens
touch lib/views/screens/first_page.dart
touch lib/views/screens/second_paage.dart
lib/views/screens/first_page.dart
import 'package:flutter/material.dart';

class FirstPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return const Scaffold(
      body: const Center(
        child: const Icon(
          Icons.face,
          size: 200,
        ),
      ),
    );
  }
}
lib/views/screens/second_paage.dart
import 'package:flutter/material.dart';

class SecondPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return const Scaffold(
      body: const Center(
        child: const Icon(
          Icons.fastfood,
          size: 200,
        ),
      ),
    );
  }
}

フッター切り替え状態管理用の class を用意する

標準パッケージの変更通知APIを提供する ChangeNotifier を継承した class を定義します。
provider パッケージなどはこの標準パッケージの ChangeNotifier などを利用して
状態管理を実現しているんですね。

特に難しい実装はないです。
setter と getter を定義し setter を使ったときに状態変化通知を行います。
今回のプロジェクトではMVVMのViewModelを意識した view_models というディレクトリをきっていますが
ディレクトリ構成はなんでもいいと思います。
(なんでもいい、が一番困るのはわかります笑)

mkdir lib/view_models
touch lib/view_models/bottom_navigation_model.dart
lib/view_models/bottom_navigation_model.dart
import 'package:flutter/material.dart';

class BottomNavigationModel extends ChangeNotifier {
  int _currentIndex = 0;

  int get currentIndex => _currentIndex;

  set currentIndex(int index) {
    _currentIndex = index;
    notifyListeners();
  }
}

BottomNavigationBar をもった StatelessWidget を定義する

前述の ChangeNotifier を継承した class
(= 今回は BottomNavigationModel) を利用して
provider パッケージの ChangeNotifierProvider<T>Consumer<T> を使った
StatelessWidget を定義します。

BottomNavigationBarBottomNavigationBarItem でフッタを構成し、
フッター切り替え時の画面がそれぞれ body に呼び出されています。

ChangeNotifierProvider<T>BottomNavigationModel の監視を開始して
Consumer<T> が状態の変更を builder に通知することで
StatelessWidget でも状態変化による再描画が実行されるんですね。

今回の記述するコード自体はシンプルです。
provider パッケージがよしなにやってくれる部分が大きいからですね。

touch lib/views/screens/root_page.dart
lib/views/screens/root_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../../view_models/bottom_navigation_model.dart';
import 'first_page.dart';
import 'second_paage.dart';

class RootPage extends StatelessWidget {
  final List<Widget> _pageList = <Widget>[
    FirstPage(),
    SecondPage(),
  ];

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<BottomNavigationModel>(
      create: (_) => BottomNavigationModel(),
      child: Consumer<BottomNavigationModel>(
        builder: (context, model, child) {
          final tabItems = [
            const BottomNavigationBarItem(
              icon: Icon(Icons.face),
              label: '',
            ),
            const BottomNavigationBarItem(
              icon: Icon(Icons.fastfood),
              label: '',
            ),
          ];

          return Scaffold(
            body: _pageList[model.currentIndex],
            bottomNavigationBar: BottomNavigationBar(
              currentIndex: model.currentIndex,
              onTap: (index) {
                model.currentIndex = index;
              },
              items: tabItems,
            ),
          );
        },
      ),
    );
  }
}

runApp() で実行する StatelessWidget をつくる

最後に View のエントリポイントをつくります。
こういうサンプルアプリの記事では
エントリポイントの MaterialApphome に直接 Widget を構築するものが多いのですが
私は initialRouteroutes を定義して
ベースとなる Widget を呼び出すような作りにしておくことが多いです。
後から何かと改修しやすいのでこうすることが多いですね。
(ベースにする MaterialApp にはテーマの情報やページ切り替え時のテーマを指定するので
それ以外の情報を書きたくないのも理由です)

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

import 'views/screens/root_page.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        pageTransitionsTheme: const PageTransitionsTheme(
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
            TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(),
          },
        ),
      ),
      darkTheme: ThemeData(brightness: Brightness.dark),
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (_) => RootPage(),
      },
    );
  }
}

ここまでできたら flutter run することで
前述の 出来上がりのイメージ に出したような画面が実行できるはずです。
おつかれさまでした。

今回のリポジトリはこちらです。

参考