【2022年最新】Flutter × Riverpod の基本的な使い方解説!

Flutterを勉強していてRiverpod の名前をよく聞くけれど、一体何なんだろう?

使い方が難しそうでわからないわ!

そんな疑問、悩みにお答えします!

本記事は、Flutter × Riverpodの基本的な使い方の解説記事となります。
サンプルアプリを基にRiverpodにおける状態の共有、参照、更新の方法を解説します。

記事後半では、Flutter大学で公開している、
Flutter × Riverpodのサンプルリポジトリを紹介します。

ぜひ読んでRiverpodの使い方を学んでみてください!

Riverpod とは?

背景

Riverpodについて解説する前に、まず背景として解決したい問題を紹介します。

例えば、カウンターアプリのカウンターの値を管理するクラスがあったとして、
このインスタンスをアプリの色々なところで使いたい、
そんな時どうすれば良いでしょうか?

思いつく方法としては、
以下の図のように main.dartでインスタンス(Instance)を定義し、
Widget ツリーの下層にインスタンスを受け渡して使用する方法です。

これだと、

  • 何度もインスタンスの受け渡しコードを書かなければならない
  • ツリーが深くなれば深くなるほどインスタンスの受け渡しが大変になる
  • インスタンスを更新したい時には+αの対応が必要になる

といった問題を抱えています。

このように、アプリ全体で共有する状態を管理することは、
アプリ開発においての大きな問題となり得ます。

これを解決する手法の一つがRiverpodです。

Riverpod による解決

Riverpod とは、上記のような状態管理に関する問題を解決するためのフレームワークです。

以下の図のようにRiverpodでは、
プロバイダと呼ばれるオブジェクトに値やインスタンスを格納し、
このプロバイダを必要な時にViewで呼び出すことで、
ツリーのどの位置でも値の呼び出し、参照を可能にします。

Riverpod のイメージ図

main.dartから値を受け渡していた時と比べて、
ずっとシンプルにできていますよね。

このように状態管理に関する問題を解決するのがRiverpodです。

Riverpod の基本的な使い方

サンプルアプリは以下の環境にて解説を行います。

Flutter 3.0.1
flutter_riverpod 1.0.4

今回は少し改造したカウンターアプリでRiverpodの基本的な使い方を学びましょう。

基本的なカウンターアプリに、画面遷移を追加したサンプルです。

コードは以下になります。

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        title: const Text('First Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'ボタンを押した回数',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).push<void>(
                  MaterialPageRoute(
                    builder: (context) => const MySecondPage(),
                  ),
                );
              },
              child: const Text('次のページ'),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _counter++;
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.red,
        title: const Text('Second Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'ボタンを押した回数',
                ),
                Text(
                  'ここに回数を表示',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text('前のページ'),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    );
  }
}

このアプリに追加したい機能としては、以下になります。

  • 画面遷移後の「ここに回数を表示」にカウンターの値を表示する
  • 画面遷移後のページでボタンを押すことによりカウントアップを行う
    画面を戻った際、このカウントアップが反映されているようにする

上記を満たすためには、2つの画面で『カウントの値』という状態を共有し、
どちらの画面からでも更新できるようにする必要があります。

この問題を、Riverpodを用いて解決しましょう。
解決を通してRiverpodの基本的な使い方を解説していきます。

パッケージのインストール

まず、パッケージをインストールしましょう。

Riverpod には使用用途に応じて3種類のパッケージが用意されています。

  • riverpod パッケージ ← Dartのみ使用する際に使用
  • flutter_riverpod パッケージ ← Flutter のみ使用する際に使用
  • hooks_riverpod パッケージ ← flutter_hooks とRiverpodを併用する際に使用

今回はFlutterのみの使用のため、flutter_riverpod パッケージを用います。

Flutterプロジェクトのルートディレクトリにて、
以下のコマンドを打ちパッケージをインストールしましょう。

flutter pub add flutter_riverpod

パッケージのインポート

パッケージのインポートを行います。

main.dartのファイルのimport部分に以下のコードを追加してください。

import 'package:flutter_riverpod/flutter_riverpod.dart';

ProviderScopeの設定

Riverpodを使う上で大事なポイントです。
Riverpodで状態管理するWidgetProviderScopeで囲う必要があります。

具体的には、1番根元の部分、main関数内でProviderScopeを以下のように設定します。

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

プロバイダの定義

いよいよ状態を格納するプロバイダを定義します。

Riverpodでは、様々な種類のプロバイダが用意されています。
(詳細はこちらをご覧ください。)

今回は、シンプルに状態を管理することのできるStateProviderを用います。

main関数の上あたりのグローバルの位置に、以下の文を追記しプロバイダを定義します。

final countProvider = StateProvider((ref) => 0);

int型の値、0を格納したStateProvidercountProviderに定義したコードとなります。

グローバルに定義して良いの?
と思われるかもしれませんが、問題有りません。
値の保存自体はローカルのスコープ内で行っています。
プロバイダ自体は不変(イミュータブル)であり、
関数をグローバルで宣言しているのと変わりないためです。(引用元
参考記事

値の参照

上記で定義したプロバイダの値を参照する方法について解説します。

プロバイダを利用するには、WidgetRef クラスのオブジェクトを取得する必要があります。

このWidgetRef クラスのオブジェクトを取得する方法については色々ありますが、
今回はConsumerWidgetを継承したWidgetにて使用する方法を紹介します。
(他の方法はこちら

まず、プロバイダで値を参照するWidgetConsumerWidgetを継承したWidgetに書き換えます。
int _counter = 0;を削除、StatefulWidgetStatelessWidgetに書き換え、
StatelessWidgetConsumerWidgetに置き換えます。)

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

  @override
  Widget build(BuildContext context,WidgetRef ref) {
    return Scaffold(

この時、build メソッドの引数にWidgetRefの変数が追加されるのに注意です。

これでプロバイダを参照する準備ができました。

_counterで値を表示していた部分をref.watch(countProvider)に変更しましょう。

Text(
  '${ref.watch(countProvider)}',
  style: Theme.of(context).textTheme.headline4,
),

ref.watch は引数のプロバイダの値を取得し、監視します。
プロバイダの値に変更があった場合、この値に依存するWidgetの更新が行われます。

つまりプロバイダの値を更新するだけで、
setStateをしなくても値が更新されて表示されるわけです。

ref.watch は引数のプロバイダーの値を取得し、監視する、ということを覚えておきましょう。

SecondPageも同様にConsumerWIdgetを継承したWidgetに書き換え、
「ここに回数を表示」の部分にref.watch(countProvider)を設定しましょう。
(最後に修正したサンプルコードを紹介します。)

値の更新

値の更新も同様に、WidgetRef クラスのオブジェクトを利用します。

値の更新ではref.readを用います。
ref.readは値の監視を行わず、取得のみを行います。
ref.watchでも値の更新が可能ですが、onPressed のようなプロパティ内で使用する場合は
ref.readの使用が推奨されています。
参考

MyHomePagesetState_counterの値を更新していたコードを、
ref.read(countProvider.notifier).state ++ に変更してください。

floatingActionButton: FloatingActionButton(
  onPressed: () {
    ref.read(countProvider.notifier).state ++;
  },
  child: const Icon(Icons.add),
),

MySecondPageも同様にref.read(countProvider.notifier).state ++を追加します。

以上がRiverpodの基本的な使い方となります!

サンプルコード

Riverpodを使って機能追加したコードの全体は以下の通りです。

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

final countProvider = StateProvider((ref) => 0);

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        title: const Text('First Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'ボタンを押した回数',
                ),
                Text(
                  '${ref.watch(countProvider)}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).push<void>(
                  MaterialPageRoute(
                    builder: (context) => const MySecondPage(),
                  ),
                );
              },
              child: const Text('次のページ'),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(countProvider.notifier).state++;
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.red,
        title: const Text('Second Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'ボタンを押した回数',
                ),
                Text(
                  '${ref.watch(countProvider)}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text('前のページ'),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(countProvider.notifier).state++;
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

上記コードを実行した結果がこちらです。

状態の共有ができており、更新がどちらの画面でもできています。

Flutter大学のサンプルリポジトリ

サンプルリポジトリのアプリ

Flutter大学で用意しているFlutter × Riverpod のサンプルリポジトリはこちらです。

GitHub - flutteruniv/flutter_riverpod_sample: flutter_riverpodの最小限のサンプル(カウンターアプリ)
flutter_riverpodの最小限のサンプル(カウンターアプリ). Contribute to flutteruniv/flutter_riverpod_sample development by creating an account on GitHub.

有志のFlutter大学メンバーにより更新がなされているリポジトリです。

サンプルリポジトリのコードにはコメントが事細かに記載されており、
このリポジトリだけでもRiverpodの使い方を学ぶことができます。

このリポジトリでは、上のサンプルアプリとは異なり、
プロバイダとしてStateNotifierProviderを用いて構築されています。

StateNotifierProviderは、
StateNotifierを継承したクラスを状態として格納するプロバイダです。
「イミュータブル(不変)」な状態を扱いたい際、
状態変更のメソッドを一つの場所で集中的に管理したい際に用いられます。
StateNotifierを継承したクラスにて状態を更新するメソッドを用意することで、
外部からメソッドを使って状態を更新することが可能となります。

例:サンプルリポジトリ、lib/state/counter.dart内のincrementCounter()メソッド

Flutter と Riverpodの組み合わせをさらに学ぶのに良い教材となっているかと思います。
本記事と併せて、ぜひ見てみてください!

まとめ

本記事では、Flutter × Riverpodの基本的な使い方の解説しました。
サンプルアプリを基にRiverpodにおける状態の共有、参照、更新の方法を解説しました。

記事後半では、Flutter大学で公開している、
Flutter × Riverpodのサンプルリポジトリを紹介しました。

今回紹介した内容は、あくまで基本的な使い方になります。
Riverpodをより深く学んでいくとさらにできることが増え、
あなたのアプリ開発の役に立つことでしょう。

本記事で紹介した内容以上に深く知りたい方は、ぜひ公式ドキュメントを読んで見てください。
(なんと、日本語化されています!)

Riverpod
A boilerplate-free and safe way to share state

本記事があなたのアプリ開発の一助となれば幸いです。

Flutterを一緒に学んでみませんか?
Flutter エンジニアに特化した学習コミュニティ、Flutter大学への入会は、
以下の画像リンクから。



編集後記(2022/5/29)

Flutter × Riverpod の解説記事でした。

先日、このようなツイートがありました。

Flutter大学のFlutter × Riverpod のサンプルについて、貴重なご意見をいただきました。

こちらについて、真摯に受け止め、
改めて正しく、最新のRiverpodの使い方を紹介した次第です。

今後はさらに注意の上、
正しい情報、新しい情報を伝えていくよう心がけていきます。

精進致しますので、
ぜひFlutter大学、週刊Flutter大学をref.watchしていてください。

週刊Flutter大学では、Flutterに関する技術記事、Flutter大学についての紹介記事を投稿していきます。
記事の更新情報はFlutter大学Twitterにて告知します。
ぜひぜひフォローをお願いいたします。

タイトルとURLをコピーしました