🥏

BottomNavigationBar を flutter_riverpod で制御してみた

2021/01/22に公開

やってみたらできました

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
  flutter_riverpod: ^0.12.2

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,
        ),
      ),
    );
  }
}

BottomNavigationBar をもった StatelessWidget を定義する

前回の記事 では ChangeNotifier を継承した Notifier クラスを用意しましたが、
今回は Riverpod の StateProvider を使います。

タブの選択値程度ならクラスを用意するまでもないのでenumを直接監視させちゃいましょう。
Provider を global に置いちゃうのが Riverpod の面白いところですね。

import 'package:flutter_riverpod/flutter_riverpod.dart';

final pageTypeProvider = StateProvider<PageType>((ref) => PageType.first);

enum PageType {
  first,
  second,
}

小見出しに「BottomNavigationBar をもった StatelessWidget を定義する」と書きまして
実際にそう書くのですが、
Widget 配下で使っている Riverpod の Consumer は StatefulWidget を継承した物なので
もうこれ何をやりたいのかわかんねぇな?
(とはいえ State の管理を Riverpod に任せて隠蔽できるので直接 StatefulWidget を使うのとは管理コストが全然違いますね)

というわけでこの部分のコード全体はこんな感じになります。

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

import 'first_page.dart';
import 'second_paage.dart';

final pageTypeProvider = StateProvider<PageType>((ref) => PageType.first);

enum PageType {
  first,
  second,
}

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

  
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, watch, child) {
        final pageType = watch(pageTypeProvider);

        final tabItems = [
          const BottomNavigationBarItem(
            icon: Icon(Icons.face),
            label: '',
          ),
          const BottomNavigationBarItem(
            icon: Icon(Icons.fastfood),
            label: '',
          ),
        ];

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

Dart の enum のIndex値は index プロパティで取得できます。
逆に、int を enum にするには values の index を指定してやればできます。

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

最後に View のエントリポイントをつくります。
Riverpod の Provider を監視するために Widget を ProviderScope() で包んでやります。

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

import 'views/screens/root_page.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return ProviderScope(
      child: 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 することで
前述の 出来上がりのイメージ に出したような画面が実行できるはずです。
おつかれさまでした。

今回のリポジトリはこちらです。
https://github.com/JUNKI555/flutter_practice07

参考

Discussion