はじめに
初投稿となります。
株式会社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は利用しないのでriverpod
とflutter_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';
プロバイダーの定義
今回はChangeNotifier
+ChangeNotifierProvider
を利用します。
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を継承してウィジェットを作成するところをConsumerWidget
、ConsumerStatefulWidget+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は同じ作者のProvider
やStatefulWidget
などの他の状態管理手法に比べて様々なメリット・デメリットがあります。
メリット
-
簡潔に書くことができる
-
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では、上の階層でプロバイダーが定義されていないにもかかわらずプロバイダーを呼び出すことが可能でした。その際は
デメリット
- 自由に記述が出来過ぎる
- ProviderやStatefulWidgetは、1つの画面(やウィジェット)に付き1つの状態管理を持つことが基本です。また、その記述方法や手法が限られていました。(Providerは
stream
を用いるBLoCモデルかChangeNotifier+Consumer
の方法) - Riverpodの場合は、Provider的な使い方も勿論可能ですが、使い回しを考慮に入れて適用範囲を細かくすることが可能です。また、ChangeNotifier、StateNotifier、Streamなどの更新を通知するための様々な仕組みも利用可能です。その上、プロバイダーをどこでも定義可能なので、トップレベルの変数に定義するだけでなくクラスのstatic変数として定義することも可能です。
-
Provider
内でProviderRef
を利用しプロバイダー同士の合成や編集が可能であり、ProviderScope
を用いてプロバイダーの中身を上書きすることもできます。 - 個人開発の場合は問題ないですが、複数人のプロジェクトでの開発となると自由に記述ができてしまうことは可読性を下げることになりますし、不具合の温床となってしまいます。
- ProviderやStatefulWidgetは、1つの画面(やウィジェット)に付き1つの状態管理を持つことが基本です。また、その記述方法や手法が限られていました。(Providerは
ポリシーを決めて利用しよう
Riverpodでは様々なことができる分、自由に記述が出来過ぎるので複数人での開発になると開発が進むにつれてソースコードの統一性が失われる危険性があります。
そのため開発前にこういった方法で開発してこう
というポリシー決めが必要になります。
例えば、下記のようなイメージです。
Riverpod+ChangeNotifier
で画面単位に状態を管理
もしくは、
Riverpod+StateNotifier+feezed
でモデル別で状態を管理
他にもChangeNotifier/StateNotifierとそのプロバイダーの定義は1つのファイルにする
、状態管理用のファイルはmodelフォルダ
以下に置く、といったファイルやフォルダ構成も予め決めておいたほうがよいでしょう。
余力があれば、テンプレートやフレームワークのようなものを作成しriverpodの使い方を制限する方法も有効かと思います。
私の場合は、Firestoreを利用することが多いのでドキュメントやコレクション単位でデータを扱うようにしChangeNotifier
をベースにモデルを扱うabstractなクラスを作成。
それを継承して利用することでデータの範囲を制限しています。
まとめ
いかがでしたでしょうか?
RiverpodはProviderやStatefulWidgetで不便だった部分を便利にしてくれる神パッケージです。
使い方のポリシーさえ作っておけば複数人での開発でもこれまでよりもスムーズに開発が進むはずです。
適切に扱って楽しい開発ライフを過ごしてください。
株式会社ORPHEではFlutterエンジニアを募集しています。
興味ある方は是非応募していただけると幸いです。