🦔

【Flutter】Riverpodを使って複数画面で再描画を行う【初心者向け】

2022/11/09に公開

こんな方におすすめの記事です。

  • Riverpodを使って複数画面で画面の再描画(リビルド)を実現したい
  • Providerの使い方を知っているが、上位互換のRiverpodの使い方が分からない

素のProviderを使った複数画面における状態更新を行う下記の記事がいまだによく見られています。

【Flutter】Providerを使って複数画面で再描画を行う【初心者向け】

ダウンロード.gif

今回は、これをRiverpodで実現してみます。

上位互換とも言えるRiverpodが登場してからだいぶん経ち、いまさら感はありますが初学者向けに実装手順を書いてみたいと思います。

Riverpodとは

Riverpodとは、「状態管理」に使われるパッケージのこと。

元々製作者のRemi RousseletさんがProviderという状態管理パッケージを公開しておりましたが、Providerの弱点というか、懸念点であったことをRiverpodであれば解消できるため、RiverpodはProviderの上位互換と言えます。

Providerパッケージは使用しませんが、RiverpodでもProviderという概念は存在するので以降混乱しないように注意してください。

Riverpodのメリット

公式ページに記載ありますが、もっと簡単に言うと、以下が挙げられます。

  • アプリの様々な場所から状態(ステート)にアクセスできる
  • プロバイダの管理する状態と状態を組み合わせて別の状態へと変換したりできる
  • 再描画する範囲を限定的にできる
    →表示速度の向上が期待できる
  • アプリのテスト容易性(テスタビリティ)を高めてくれる
  • データ取得、更新中、データ取得エラーなどの取り扱いが楽にできる

Riverpodイメージ

まずは比較するために通常のProviderパッケージのイメージから確認します。

Providerイメージ

上図の通り、通常のProviderパッケージでは、更新したいWidgetの親となる位置に、Providerを配置する必要がありました。
これだと、対象のProviderの配下にないWidgetはProviderの管理する状態にアクセスすることができませんでした。

それがRiverpodだと下図のようになります。

Riverpodイメージ

Providerをグローバルに宣言することで、どこからでもProviderの管理する状態にアクセスすることができます。

Riverpodパッケージ

Riverpod には複数の種類のパッケージがあり、それぞれ使用目的が若干異なります。

パッケージ名 説明
riverpod Flutterに関連するクラスを除外したDartのみで使用する場合のパッケージ
flutter_riverpod FlutterアプリでRiverpodを使用する場合のパッケージ
hooks_riverpod flutter_hooksと併用する場合のパッケージ

本記事は、FlutterでのRiverpod使用法について解説するため、flutter_riverpod、もしくはhooks_riverpodを使用することになりますね。

両者に関しては使い方や記述に大きな違いはなく、hooks_riverpodのほうが後々できることが多いため、本記事では、hooks_riverpodを使用していきます。

Riverpod-Providerの種類

Riverpodには様々なProviderが用意されており、それぞれに適した場面があります。

Providerの種類 説明
Provider 基本的なProvider。外からは状態を変更できない
StateProvider シンプルな状態を保持するするのに適したProvider
StateNotifierProvider メソッドを実装し、複数状態を保持するのに適したProvider
FutureProvider 非同期処理結果を受け取るのに適したProvider
StreamProvider 非同期処理結果を随時受け取る(Stream)のに適したProvider
ChangeNotifierProvider メソッドを実装し、複数状態を保持するのに適したProvider。StateNotifierProviderとは違って、ミュータブルな状態と操作するメソッドを同一クラスに定義する

はじめから使い分けるのは難しいかもしれないので、最初はProvider、StateNotifierProviderあたりが使えれば良いと思います。

そのほかは必要に迫られたときに覚えていきましょう。

provider実装手順

それでは、Providerパッケージで実現していた複数画面で再描画を行う下記のコードを、RiverpodのStateNotifierProviderを使って実現していきます。

Providerパッケージで実現していたコード

実現している動作の説明

  • ボタンタップで遷移先に移動すると、表示値_resultの状態を保持するResultProviderにて表示を更新。
    遷移先では更新された値「遷移先に移動」が表示される。
  • ボタンタップまたは、ナビゲーションバーから戻るときに、ResultProviderにて表示を更新。
    遷移先では更新された値「Thakyou!! from 戻るアイコン」が表示される。
main.dart


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

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<ResultProvider>(
          create: (context) => ResultProvider(),
        ),
      ],
      child: MaterialApp(
        home: MyHomePage(),
      ),
    ),
  );
}

class ResultProvider extends ChangeNotifier {
  String _result;

  ResultProvider() {
    initValue();
  }

  // 初期化
  void initValue() {
    this._result = "遷移先に移動";
  }

  void refresh() {
    initValue();
    notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
  }

  void updateText(String str) {
    _result = str;
  }

  void notify() {
    notifyListeners(); // Providerを介してConsumer配下のWidgetがリビルドされる
  }
}

// 遷移元ページ
class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // テキスト表示
    Widget _renderText(ResultProvider model) {
      // print('text:${model._result}');
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              model._result, // ResultProviderのプロパティ
              style: Theme.of(context).textTheme.headline5,
            ),
            ElevatedButton(
              child: Text('Go to Edit Page'),
              onPressed: () async {
                model.updateText('Hello! from HomePage.');
                await Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) =>
                        // 引数に遷移元から遷移先へ渡す値を設定
                        EditPage(),
                  ),
                );
                // print(result);
                model.notify(); // ResultProviderのメソッド
              },
            ),
          ],
        ),
      );
    }

    // サンプル1:Scaffold全体をリビルド
    // return Consumer<ResultProvider>(builder: (context, model, _) {
    //   return Scaffold(
    //     appBar: appBar(),
    //     body: _renderText(model),
    //     floatingActionButton: FloatingActionButton(
    //       onPressed: () {
    //         model.refresh();
    //       },
    //       child: Icon(
    //         Icons.refresh,
    //       ),
    //     ),
    //   );
    // });

    // サンプル2:appBar以外をリビルド
    return Scaffold(
      appBar: appBar(),
      body: Consumer<ResultProvider>(builder: (context, model, _) {
        return _renderText(model);
      }),
      floatingActionButton:
          Consumer<ResultProvider>(builder: (context, model, _) {
        return FloatingActionButton(
          onPressed: () {
            model.refresh(); // ResultProviderのメソッド
          },
          child: Icon(
            Icons.refresh,
          ),
        );
      }),
    );
  }

  appBar() {
    print('appBar実行');
    return AppBar(
      title: Text('My Home Page(遷移元)'),
    );
  }
}

// 遷移先ページ
class EditPage extends StatelessWidget {
  final receive;
  const EditPage({Key key, this.receive}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Consumer<ResultProvider>(
      builder: (context, model, child) {
        return WillPopScope(
          onWillPop: () {
            model.updateText('Thank you! from 戻るアイコン'); // ResultProviderのメソッド
            Navigator.pop(context);
            return Future.value(false);
          },
          child: Scaffold(
            appBar: AppBar(
              title: Text('Edit Page(遷移先)'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    model._result,
                    style: Theme.of(context).textTheme.headline5,
                  ),
                  ElevatedButton(
                      child: Text('Return'),
                      onPressed: () {
                        model.updateText(
                            'Thank you! from 戻るボタン'); // ResultProviderのメソッド
                        Navigator.pop(context);
                      }),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

Riverpod版の実装手順はこちら。

  1. Riverpodパッケージをインストール
  2. MyAppの上にProviderScopeを配置
  3. StateNotifierクラスを継承したクラスを作成
  4. StateNotifierクラスのインスタンスを持つProviderをグローバルに宣言
  5. 再描画対象としたいWidgetをConsumerでラップする

順に説明します。

1.Riverpodパッケージをインストール

はじめに、flutter_hooksとhooks_riverpodをインストールします。

pubspec.yaml
dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  flutter_hooks: ^0.18.5+1
  hooks_riverpod: ^2.1.1

2.MyAppの上部にProviderScopeを配置

Riverpod版のProviderをアプリで使用可能にするために、アプリのルート、ここではMyAppの上部にProviderScopeを配置します。

main.dart
void main() {
  runApp(
    // ProviderScopeを配置
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod sample',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

3. StateNotifierクラスを継承したクラスを作成

表示する値の操作を実現するStateNotifierクラスを継承したResultNotifierを作成します。
※説明のしやすさからmain.dartに実装していますが、別のファイルに分けてもいいです。

extendsで継承しているStateNotifierのジェネリクスには保持する型(ここではString)を定義します。

main.dart

/// StateNotifierProvider に渡すことになる StateNotifier クラスです。
/// StateNotifierには保持する型(ここではString)を定義します。
class ResultNotifier extends StateNotifier<String> {
  ResultNotifier() : super(defaultResultValue);

  static const defaultResultValue = '遷移先に移動';

  void initValue() {
    // state更新時にProviderを介してConsumer配下のWidgetがリビルドされる
    state = defaultResultValue;
  }

  void updateText(String str) {
    // state更新時にProviderを介してConsumer配下のWidgetがリビルドされる
    state = str;
  }

  void refresh() {
    initValue();
  }
}

通常のProviderでは、notifyListeners()を実行して状態更新を通知する必要がありましたが、StateNofierProviderではそれが不要となり、状態(state)が更新されると自動で監視しているWidgetが再描画されるようになりました。

4. StateNotifierクラスのインスタンスを持つProviderをグローバルに宣言

StateNotifierクラスのインスタンスを持つStateNotifierProviderインスタンスをグローバルに宣言することで、アプリ内のどこからでもこのProviderが管理する状態と操作が利用できるようになります。

ここが通常のProviderパッケージと大きく異なる所ですね。

main.dart
/// 状態の保持と操作を行うProvider
/// StateNotifierを継承した操作用のクラス(ResultNotifier)と、状態の型を定義します。
final resultProvider =
    StateNotifierProvider.autoDispose<ResultNotifier, String>((ref) {
  return ResultNotifier();
});

5. 再描画対象としたいWidgetをConsumerでラップする

ここは様々な方法がありますが、元々Providerパッケージで実現したコードを元にしていきたいので、今回も状態更新時に、再描画対象としたいWidgetをConsumerでラップすることで実現してみます。

以下のようにConsumerを配置すると、Consumer配下のWidgetがリビルド対象となります。

Consumer(builder: (context, ref, child) { 
  // ref.watch()で監視するProviderの状態が更新されると、再描画されます
  final result = ref.watch(resultProvider);
}

今回はappBar以外の遷移元ページや遷移先ページで使用しています。

また、ResultNotifierクラスのメソッドを使用するには、
ref.read(resultProvider.notifier)を使ってもいいですし、何度も使用する場合は一度
final notifier = ref.watch(resultProvider.notifier);としておくのでも良いと思います。

main.dart

// 遷移元ページ
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  // テキスト表示
  Widget _renderText(BuildContext context, WidgetRef ref) {
    // resultProviderの保持する状態に変化があるとウィジェットが更新されます
    final result = ref.watch(resultProvider);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            result, // ResultProviderのプロパティ
            style: Theme.of(context).textTheme.headline5,
          ),
          ElevatedButton(
            child: const Text('Go to Edit Page'),
            onPressed: () async {
              ref
                  .read(resultProvider.notifier)
                  .updateText('Hello! from HomePage.');
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const EditPage(),
                ),
              );
            },
          ),
        ],
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Home Page(遷移元)'),
      ),
      // _renderText内でref.watch()監視するProviderの状態が更新されると再描画されます
      body: Consumer(builder: (context, ref, _) {
        return _renderText(context, ref);
      }),
      floatingActionButton: Consumer(builder: (context, ref, _) {
        return FloatingActionButton(
          onPressed: () {
            ref.read(resultProvider.notifier).refresh(); // ResultProviderのメソッド
          },
          child: const Icon(
            Icons.refresh,
          ),
        );
      }),
    );
  }
}

// 遷移先ページ
class EditPage extends StatelessWidget {
  const EditPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        // resultProviderの保持する状態に変化があるとウィジェットが更新されます
        final result = ref.watch(resultProvider);
        // StateNotifierクラスのメソッドを実行できるようになります
        final notifier = ref.watch(resultProvider.notifier);
        return WillPopScope(
          onWillPop: () {
            // ref.read(resultProvider.notifier).updateText("~~")でもOK
            notifier
                .updateText('Thank you! from 戻るアイコン'); // ResultProviderのメソッド
            Navigator.pop(context);
            return Future.value(false);
          },
          child: Scaffold(
            appBar: AppBar(
              title: const Text('Edit Page(遷移先)'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    result,
                    style: Theme.of(context).textTheme.headline5,
                  ),
                  ElevatedButton(
                      child: const Text('Return'),
                      onPressed: () {
                        // ref.read(resultProvider.notifier).updateText("~~")でもOK
                        notifier.updateText(
                            'Thank you! from 戻るボタン'); // ResultProviderのメソッド
                        Navigator.pop(context);
                      }),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

サンプルコード全文

サンプルコード全文です。

サンプルコード全文
main.dart

void main() {
  runApp(
    //
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod sample',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

/// 状態の保持と操作を行うProvider
/// StateNotifierを継承した操作用のクラス(ResultNotifier)と、状態の型を定義します。
final resultProvider =
    StateNotifierProvider.autoDispose<ResultNotifier, String>((ref) {
  return ResultNotifier();
});

/// StateNotifierProvider に渡すことになる StateNotifier クラスです。
class ResultNotifier extends StateNotifier<String> {
  ResultNotifier() : super(defaultResultValue);

  static const defaultResultValue = '遷移先に移動';

  void initValue() {
    // state更新時にProviderを介してConsumer配下のWidgetがリビルドされる
    state = defaultResultValue;
  }

  void updateText(String str) {
    // state更新時にProviderを介してConsumer配下のWidgetがリビルドされる
    state = str;
  }

  void refresh() {
    initValue();
  }
}

// 遷移元ページ
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  // テキスト表示
  Widget _renderText(BuildContext context, WidgetRef ref) {
    // resultProviderの保持する状態に変化があるとウィジェットが更新されます
    final result = ref.watch(resultProvider);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            result, // ResultProviderのプロパティ
            style: Theme.of(context).textTheme.headline5,
          ),
          ElevatedButton(
            child: const Text('Go to Edit Page'),
            onPressed: () async {
              ref
                  .read(resultProvider.notifier)
                  .updateText('Hello! from HomePage.');
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const EditPage(),
                ),
              );
            },
          ),
        ],
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Home Page(遷移元)'),
      ),
      body: Consumer(builder: (context, ref, _) {
        return _renderText(context, ref);
      }),
      floatingActionButton: Consumer(builder: (context, ref, _) {
        return FloatingActionButton(
          onPressed: () {
            ref.read(resultProvider.notifier).refresh(); // ResultProviderのメソッド
          },
          child: const Icon(
            Icons.refresh,
          ),
        );
      }),
    );
  }
}

// 遷移先ページ
class EditPage extends StatelessWidget {
  const EditPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        // resultProviderの保持する状態に変化があるとウィジェットが更新されます
        final result = ref.watch(resultProvider);
        // StateNotifierクラスのメソッドを実行できるようになります
        final notifier = ref.watch(resultProvider.notifier);
        return WillPopScope(
          onWillPop: () {
            // ref.read(resultProvider.notifier).updateText("~~")でもOK
            notifier
                .updateText('Thank you! from 戻るアイコン'); // ResultProviderのメソッド
            Navigator.pop(context);
            return Future.value(false);
          },
          child: Scaffold(
            appBar: AppBar(
              title: const Text('Edit Page(遷移先)'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    result,
                    style: Theme.of(context).textTheme.headline5,
                  ),
                  ElevatedButton(
                      child: const Text('Return'),
                      onPressed: () {
                        // ref.read(resultProvider.notifier).updateText("~~")でもOK
                        notifier.updateText(
                            'Thank you! from 戻るボタン'); // ResultProviderのメソッド
                        Navigator.pop(context);
                      }),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

さいごに

Riverpod版のProviderは通常のProviderパッケージに比べると記述量が増えるので最初は難しく感じるかもしれませんが、何度か書いていくと慣れてくると思います。

BLEのビーコンとRiverpodを使用した書籍を書いております。
はじめにStatefulWidgetで実現してから、Riverpodにリファクタリングしており、Riverpodの使い方についてさらに学びたい方におススメです。
※基礎レベルの内容です。

https://zenn.dev/mamoru_takami/books/dd632291d4ecc8

Discussion