6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Riverpod1.0.0の紹介と使い方

Last updated at Posted at 2021-12-28

はじめに

初投稿となります。
株式会社ORPHE(オルフェ)の廣瀬です。

今回は先月メジャーバージョンにアップデートされたRiverpodについて特徴や使い方などを紹介したいと思います。

Riverpodとは

Flutterに必要不可欠な状態管理&更新ライブラリのひとつです。

公式では状態を管理&更新するために**StatefulWidget&State**を利用することが基本となっておりますが、外部データベースからのデータを反映させる場合など実装が難しい場面によく遭遇します。

そのため、BLoCパターンやRedux、MobXなど様々なパッケージや手法が生まれてきました。

その中でも特に有名なProviderというパッケージがあり、その同一作者による事実上の上位互換パッケージとして生まれたのがこのRiverpodです。

Riverpodの使い方

このRiverpodのFlutterでの使い方を皆さんにお馴染みのカウンターアプリのサンプルで説明したいと思います。

インポート

RiverpodにはDartで使うためのriverpod、Flutterで使うためのflutter_riverpod、flutter_hooksのパッケージと一緒に使うためのhooks_riverpodのパッケージがあります。

今回はflutter_hooksは利用しないのでriverpodflutter_riverpodのパッケージをインポートします。

# pubspec.yaml
...

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  riverpod: ^1.0.0
  flutter_riverpod: ^1.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

...

dartファイルでは以下のように記載してインポートします。

import 'package:riverpod/riverpod.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

プロバイダーの定義

今回はChangeNotifierChangeNotifierProviderを利用します。

ChangeNotifierはStatefulWidgetのsetState()と同じようなイメージで、notifyListeners()をコールすることによりプロバイダーに更新を通知することができます。

まずChangeNotifierを継承してカウントアップを行うためのCounterクラスを作成します。

// main.dart

// カウントアップ用のChangeNotifierの定義。
class Counter extends ChangeNotifier {
	// カウントを初期化。
	int count = 0;
	// カウントアップ。
	void increment() {
		count = count + 1;
		notifyListeners();
	}
}

Counterを利用するためのプロバイダーを作成します。

プロバイダーはどこでも作成することができ、公式ではトップレベルの変数で定義することを推奨しています。

// プロバイダーの定義。
final counterProvider = ChangeNotifierProvider((ref) => Counter());

プロバイダーの利用

プロバイダーの準備ができたので実際のウィジェット内で利用していきます。

まず下準備としてProviderScopeをMaterialAppの上の階層に配置します。

このProviderScopeの中に現在利用されている有効なプロバイダーの実データが保存されており、これがあることで各ウィジェット間でプロバイダーの値を相互利用することが可能になります。

// main.dart

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    // MaterialAppの上にProviderScopeを定義。
    return ProviderScope(
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          // This is the theme of your application.
          //
          // Try running your application with "flutter run". You'll see the
          // application has a blue toolbar. Then, without quitting the app, try
          // changing the primarySwatch below to Colors.green and then invoke
          // "hot reload" (press "r" in the console where you ran "flutter run",
          // or simply save your changes to "hot reload" in a Flutter IDE).
          // Notice that the counter didn't reset back to zero; the application
          // is not restarted.
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(),
      ),
    );
  }
}

プロバイダーを利用するために通常StatelessWidgetやStatefulWidget+Stateを継承してウィジェットを作成するところをConsumerWidgetConsumerStatefulWidget+ConsumerStateを継承してウィジェットを作成します。

するとWidgetRefというオブジェクトを利用することができるようになるのでそれを用いてプロバイダーを扱います。

ConsumerWidgetの場合は下記のようになります。

// main.dart

// ComsupereWidgeetを利用
class MyHomePage extends ConsumerWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // プロバイダーの監視と取得。
    final counter = ref.watch(counterProvider);

    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text("app"),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counter.count}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counter.increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

buildメソッドの引数でWidgetRefが与えられるようになり、主に下記のメソッドが使えるようになります。

final データ = ref.watch(プロバイダー)

プロバイダーを監視しプロバイダーから与えられた中のデータを取得します。

プロバイダーが更新されると呼び出されたウィジェットが更新されます。

final データ = ref.read(プロバイダー)

プロバイダーから与えられた中のデータを取得します。
プロバイダーが更新されても呼び出されたウィジェットは更新されません。
プロバイダーの更新に合わせてウィジェットの描画を更新するか否かによって使い分けます。

main.dartをまとめると以下のようになります。

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

// カウントアップ用のChangeNotifierの定義。
class Counter extends ChangeNotifier {
  // カウントを初期化。
  int count = 0;
  // カウントアップ。
  void increment() {
    count = count + 1;
    notifyListeners();
  }
}

// プロバイダーの定義。
final counterProvider = ChangeNotifierProvider((ref) => Counter());

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    // MaterialAppの上にProviderScopeを定義。
    return ProviderScope(
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          // This is the theme of your application.
          //
          // Try running your application with "flutter run". You'll see the
          // application has a blue toolbar. Then, without quitting the app, try
          // changing the primarySwatch below to Colors.green and then invoke
          // "hot reload" (press "r" in the console where you ran "flutter run",
          // or simply save your changes to "hot reload" in a Flutter IDE).
          // Notice that the counter didn't reset back to zero; the application
          // is not restarted.
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ComsupereWidgeetを利用
class MyHomePage extends ConsumerWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // プロバイダーの監視と取得。
    final counter = ref.watch(counterProvider);

    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text("app"),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counter.count}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counter.increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

その他プロバイダー

StateNotifierProvider

StateNotifierを扱うためのプロバイダーです。

StateNotifierはChangeNotifierと違い状態を1つの値(オブジェクト)に絞って管理します。

freezedと合わせて利用することで状態の変化をシンプルにわかりやすく扱うことが可能なためriverpod+StateNotifier+freezedの組み合わせで扱うことも多いです。

StateNotifierとStateNotifierProviderの定義は下記のように行います。

ポイントとしては下記です。

  • StateNotifier<int>のように扱う値のタイプを明示的に定義
  • コンストラクタで初期化
  • stateに新しい値を再代入することで状態を更新&更新を通知

またバージョン0.14.0からStateNotifierProvider<CounterController, int>のようにStateNotifierのタイプに加えてStateNotifier内で扱う値のタイプも合わせて明記します。

// カウントアップ用のStateNotifierの定義。
class CounterController extends StateNotifier<int> {
  CounterController(): super(0);

  /// カウントを一個増やす
  void increment() {
		state = state + 1;
	}
}

// プロバイダーの定義。
final counterControllerProvider = StateNotifierProvider<CounterController, int>((ref) => CounterController());

また、利用する際は扱う値とStateNotifer自体を分離して取得します。

// StateNotifierの値の取得。
final count = ref.watch(counterControllerProvider);
/// StateNofitier自体の取得。
final counterController = ref.read(counterControllerProvider.notifier);

FutureProvider

非同期処理を待つためのFuture型を扱うためのプロバイダーです。

FlutterにはFutureBuilderというウィジェットが存在しますが、それと同じようにFutureが完了した際に画面を更新することが可能です。

// Providerの中でasync/awaitを使って書くことも可能
final futureProvider = FutureProvider((ref) async {
  print("5秒後に10を返すよ");
  await Future.delayed(const Duration(seconds: 5));
  print("5秒経ちました");
  return 10;
});

FutureProviderと同じようにStreamを扱うためのStreamProviderも存在します。

Provider

riverpodを利用しているとプロバイダーの値を画面ごとに編集したり、プロバイダーの値を合成したいという欲求が湧いてきます。

この場合Providerを用いることでその思いに応えることができます。

各種プロバイダーの引数としてProviderRefというものが与えられており、前述したWidgetRefと同じようにref.watch()ref.read()が利用可能になっています。それらを駆使することにより、各プロバイダーの更新通知をコントロールしながら値を編集したり合成することが可能です。

// 前述のcounterProviderの値を10倍にして返すプロバイダー。
final provider = Provider((ref) {
	// counterProviderの更新通知を受け取り、更新時にこのプロバイダーも更新する。
  final counter = ref.watch(counterProvider);
  return counter.count * 10;
});
// ComsupereWidgeetを利用
class MyHomePage extends ConsumerWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 10倍にしたカウントを取得。
    final counter = ref.watch(provider);
		// カウントをコントロールするために元のcounterProviderも取得。
    final counterController = ref.read(counterProvider);

    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text("app"),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${counter}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counterController.increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Riverpodのメリット・デメリット

Riverpodは同じ作者のProviderStatefulWidgetなどの他の状態管理手法に比べて様々なメリット・デメリットがあります。

メリット

  • 簡潔に書くことができる

    • Providerの場合、プロバイダーを利用するには上の階層でプロバイダーを定義するためのウィジェットを書く必要がありました。また、更新を監視するためにConsumerウィジェットを利用しなければいけないなどネストが深くなりがちでした。
    • riverpodの場合はWidgetRefで状態の読み込みと監視・更新が行えるのでより簡潔に書くことができます。
  • 使いまわししやすい

    • StatefulWidgetなどはそのウィジェット用のStateを定義しなければならず他のウィジェットで状態管理を使い回すことが困難でした。
    • riverpodの場合は、familyを利用することで同じ型のプロバイダーでも個別のオブジェクトとして利用することが可能になっています。
    • 例えば、Firestoreのドキュメントを管理するプロバイダーを作成しておき、そのドキュメントパスをfamilyの引数にしておけば異なるドキュメントを同じプロバイダーで扱うことができます。
    final firestoreDocumentProvider = ChangeNotifierProvider.family<FirestoreDocument, String>((ref, path) => FirestoreDocument(path));
    
    Widget build(BuildContext context, WidgetRef ref) {
    	final userDocument = ref.watch(firestoreDocumentProvider("user/me"));
    	final eventDocument = ref.watch(firestoreDocumentProvider("event/today"));
    	...
    }
    
  • テストが簡単

    • RiverpodはFlutterを用いずDartのみで記述することが可能です。そのためユニットテストを行うときも実装しているプロバイダーをそのまま利用することが可能です。
  • エラーが起きにくい

    • Providerでは、上の階層でプロバイダーが定義されていないにもかかわらずプロバイダーを呼び出すことが可能でした。その際はProviderNotFoundExceptionの例外が発生してしまいます。この例外の発生は上の階層を把握していないと防ぐことができずプログラムを実行しないかぎり気づくことができません。
    • Riverpodではプロバイダーが定義されていない場合、コンパイルエラーとなりIDEを用いている場合はプログラム記述中にエラーが出ます。そのため実行時のエラーを少なくすることができます。

デメリット

  • 自由に記述が出来過ぎる
    • ProviderやStatefulWidgetは、1つの画面(やウィジェット)に付き1つの状態管理を持つことが基本です。また、その記述方法や手法が限られていました。(Providerはstreamを用いるBLoCモデルかChangeNotifier+Consumerの方法)
    • Riverpodの場合は、Provider的な使い方も勿論可能ですが、使い回しを考慮に入れて適用範囲を細かくすることが可能です。また、ChangeNotifier、StateNotifier、Streamなどの更新を通知するための様々な仕組みも利用可能です。その上、プロバイダーをどこでも定義可能なので、トップレベルの変数に定義するだけでなくクラスのstatic変数として定義することも可能です。
    • Provider内でProviderRefを利用しプロバイダー同士の合成や編集が可能であり、ProviderScopeを用いてプロバイダーの中身を上書きすることもできます。
    • 個人開発の場合は問題ないですが、複数人のプロジェクトでの開発となると自由に記述ができてしまうことは可読性を下げることになりますし、不具合の温床となってしまいます。

ポリシーを決めて利用しよう

Riverpodでは様々なことができる分、自由に記述が出来過ぎるので複数人での開発になると開発が進むにつれてソースコードの統一性が失われる危険性があります。
そのため開発前にこういった方法で開発してこうというポリシー決めが必要になります。

例えば、下記のようなイメージです。

Riverpod+ChangeNotifierで画面単位に状態を管理

もしくは、

Riverpod+StateNotifier+feezedでモデル別で状態を管理

他にもChangeNotifier/StateNotifierとそのプロバイダーの定義は1つのファイルにする、状態管理用のファイルはmodelフォルダ以下に置く、といったファイルやフォルダ構成も予め決めておいたほうがよいでしょう。

余力があれば、テンプレートやフレームワークのようなものを作成しriverpodの使い方を制限する方法も有効かと思います。

私の場合は、Firestoreを利用することが多いのでドキュメントやコレクション単位でデータを扱うようにしChangeNotifierをベースにモデルを扱うabstractなクラスを作成。

それを継承して利用することでデータの範囲を制限しています。

まとめ

いかがでしたでしょうか?

RiverpodはProviderやStatefulWidgetで不便だった部分を便利にしてくれる神パッケージです。
使い方のポリシーさえ作っておけば複数人での開発でもこれまでよりもスムーズに開発が進むはずです。

適切に扱って楽しい開発ライフを過ごしてください。

株式会社ORPHEではFlutterエンジニアを募集しています。
興味ある方は是非応募していただけると幸いです。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?