👐

RiverpodでBottomNavigationBarを作る

2023/08/13に公開

Riverpodでボトムナビゲーションバーを作る

StateNotifier、Notifier、enumを使いタブがタップされたら、ページを切り替える状態管理をriverpodで行うパターンを作ってみました。

Riverpodを使わない場合は、状態管理はStatefulWidgetのsetStateで行います。タブをタップすると、画面が切り替わるという単純なロジックですね。

🔰StatefulWidgetの場合

https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html

公式リファレンスの方法だと、標準WidgetをStatefulWidgetのsetStateで状態管理して使う。これが昔からあるパターンですね。初心者も理解しやすいと思います。

import 'package:flutter/material.dart';

/// Flutter code sample for [BottomNavigationBar].

void main() => runApp(const BottomNavigationBarExampleApp());

class BottomNavigationBarExampleApp extends StatelessWidget {
  const BottomNavigationBarExampleApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: BottomNavigationBarExample(),
    );
  }
}

class BottomNavigationBarExample extends StatefulWidget {
  const BottomNavigationBarExample({super.key});

  
  State<BottomNavigationBarExample> createState() =>
      _BottomNavigationBarExampleState();
}

class _BottomNavigationBarExampleState
    extends State<BottomNavigationBarExample> {
  int _selectedIndex = 0;
  static const TextStyle optionStyle =
      TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
  static const List<Widget> _widgetOptions = <Widget>[
    Text(
      'Index 0: Home',
      style: optionStyle,
    ),
    Text(
      'Index 1: Business',
      style: optionStyle,
    ),
    Text(
      'Index 2: School',
      style: optionStyle,
    ),
  ];

  void _onItemTapped(int index) {
    // setStateでタブがタップされたら、画面を更新する
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BottomNavigationBar Sample'),
      ),
      body: Center(
        child: _widgetOptions.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Business',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.school),
            label: 'School',
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

🔨ConsumerWidgetを使ったパターン

StatefulWidgetを使わないパターンですね。画面が切り替わるロジックは、Riverpodがやってくれます。
複雑な状態管理を行うときは、StateNotifierを使用する。StateProviderを使ったパターンもあるが、こちらを使った方が、複雑なコードを書かないので楽に状態管理ができる。

🔨StateNotifierで状態管理

Riverpod2.0では、非推奨ですが、比較対象としてサンプル作りました。世の中にサンプルあるのですが、説明がわかりにくいな〜と思い自分が理解できる書き方ですが作ってみました。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// [StateNotifierの場合]
void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

enum PageType {
  Home,
  Settings,
}

class PageNotifier extends StateNotifier<PageType> {
  PageNotifier() : super(PageType.Home);// 最初に表示するページを設定

  // タブが切り替わった時に呼ばれる
  void changePage(PageType pageType) {
    state = pageType;
  }
}

final pageProvider = StateNotifierProvider<PageNotifier, PageType>(
  (ref) => PageNotifier(),
);

class MyApp extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      title: 'Riverpod BottomNavigationBar',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MainPage(),
    );
  }
}

class MainPage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final currentPage = ref.watch(pageProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text(currentPage == PageType.Home ? "Home" : "Settings"),
      ),
      body: currentPage == PageType.Home ? HomeWidget() : SettingsWidget(),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentPage == PageType.Home ? 0 : 1,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'Settings',
          ),
        ],
        onTap: (index) {
          final pageType = index == 0 ? PageType.Home : PageType.Settings;
          ref.read(pageProvider.notifier).changePage(pageType);
        },
      ),
    );
  }
}

class HomeWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Center(
      child: Text('This is Home Page!'),
    );
  }
}

class SettingsWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Center(
      child: Text('This is Settings Page!'),
    );
  }
}

🔨Notifierを使ったパターン

Riverpod2.0からは、NotifierをStateNotifierとStateProviderの代わりに使うことが推奨されました。ですので、作ってみました。これを使った例は今の所記事でも見つけられていないです。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// [Notifierの場合]
void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

enum PageType {
  Home,
  Settings,
}

class BottomNavigationNotifier extends Notifier<PageType> {
  
   build() {
    return PageType.Home;// 最初に表示するページを設定
  }
  // タブが切り替わった時に呼ばれる
  void changePage(PageType pageType) {
    state = pageType;
  }
}

final bottomNavigationNotifierProvider = NotifierProvider<BottomNavigationNotifier, PageType>(BottomNavigationNotifier.new);

class MyApp extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      title: 'Riverpod BottomNavigationBar',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MainPage(),
    );
  }
}

class MainPage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final currentPage = ref.watch(bottomNavigationNotifierProvider);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.indigo,
        title: Text(currentPage == PageType.Home ? "Home" : "Settings"),
      ),
      body: currentPage == PageType.Home ? HomeWidget() : SettingsWidget(),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentPage == PageType.Home ? 0 : 1,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'Settings',
          ),
        ],
        onTap: (index) {
          final pageType = index == 0 ? PageType.Home : PageType.Settings;
          ref.read(bottomNavigationNotifierProvider.notifier).changePage(pageType);
        },
      ),
    );
  }
}

class HomeWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Center(
      child: Text('This is Home Page Notifier!'),
    );
  }
}

class SettingsWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Center(
      child: Text('This is Settings Page Notifier!'),
    );
  }
}

まとめ

StatefulWidgetの時は、static const リストの変数を定義して、タップしたら、ページを切り替えるメソッドが実行されて、状態管理はsetStateで行っています。

Riverpodの時は、enumでどこのページに切り替えるのか指定して、StateNotifierかNotifierでタブが切り替わる状態を管理しています。

おまけ

ページ3個以上に増やすときはこんな感じで書けばできます。

import 'package:chart_app/screen/page/chart/line_chart.dart';
import 'package:chart_app/screen/page/home/home_page.dart';
import 'package:chart_app/screen/page/info/InfoPage.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

enum PageType {
  Home,
  Chart,
  Info
}

class BottomNavigationNotifier extends Notifier<PageType> {
  
  build() {
    return PageType.Home;  // 最初に表示するページを設定
  }

  // タブが切り替わった時に呼ばれる
  void changePage(PageType pageType) {
    state = pageType;
  }
}

final bottomNavigationNotifierProvider = NotifierProvider<BottomNavigationNotifier, PageType>(BottomNavigationNotifier.new);

class BottomNavigationPage extends ConsumerWidget {
  const BottomNavigationPage({super.key});

  static const String relativePath = '/bottom_navigation';

  
  Widget build(BuildContext context, WidgetRef ref) {
    final currentPage = ref.watch(bottomNavigationNotifierProvider);

    String appBarTitle;
    Widget bodyWidget;
    switch(currentPage) {
      case PageType.Home:
        appBarTitle = "Home";
        bodyWidget = const HomePage();
        break;
      case PageType.Chart:
        appBarTitle = "Chart";
        bodyWidget = const LineChartPage();
        break;
      case PageType.Info:
        appBarTitle = "Info";
        bodyWidget = const InfoPage();
        break;
    }

    return Scaffold(
      body: bodyWidget,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: PageType.values.indexOf(currentPage),
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.show_chart),
            label: 'Chart',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.info),
            label: 'Info',
          ),
        ],
        onTap: (index) {
          final pageType = PageType.values[index];
          ref.read(bottomNavigationNotifierProvider.notifier).changePage(pageType);
        },
      ),
    );
  }
}

Discussion