はじめに
Providerの基本的な使い方を、備忘録としてまとめようと思います。
基本riverpod公式ドキュメントから、必要と思われる情報を抜き出して記述していますので、さらに詳しく知りたい方はこちらをご覧ください。
https://riverpod.dev/ja/docs/getting_started
また、consumerWidgetの書き換えに関しては、さくしんさんのudemy講座を参考にさせていただきました。
riverpodの書き換えだけでなく、テスト駆動開発や、MVVMの考え方などにも深く言及されているのでおすすめです。
https://www.udemy.com/course/riverpod/
Providerの種類
まずProviderの種類から。以下の7種類のproviderがあるが、初めのうちはstateProviderを使うことが多い。FutureProvider、StreamProviderに関してはfirebaseを使用する際によく使う。
- Provider…定数
- StateProvider…変数
- ScopedProvider…出力
- StateNotifierProvider…メソッドのついたprovider
- FutureProvider…外部からデータを取ってきて、後で参照する時に使用
- StreamProvider…外部からデータを引っ張ってくる時に使用
Providerを使うメリット
- 値にアクセスしやすい。グローバルに定義されるので、バケツリレーみたいにページ間で値をやりとりしなくて済む
- 画面更新の最適化ができる。更新範囲を決めることができるので、描画にかかる時間を減らすことができる。
- いくつかの値を組み合わせて、別の値として定義しやすい。
Providerの始め方
とりあえずグローバル変数として定義して、いろんなファイルから呼び出すのがproviderの主な使い方。
どのproviderも「ref」というオブジェクトを引数にして受け取る。一般的に、refはウィジェットからプロバイダに渡される。
final myProvider = Provider((ref) => return MyValue());
refの使い道
refには以下の3種類の使い方がある。
- ref.watch
- ref.listen
- ref.read
しかし、そのうちref.readは公式では推奨されていない。
ref.read はリアクティブではないため、可能な限り使用を避けてください。
watch や listen の使用では問題が生じる場合の回避策として存在しています。 ほとんどの場面では watch や listen の使用、特に watch の使用がベターなはずです。
ref.watch
プロバイダの値を取得した上で、その変化を監視する。値が変化すると、その値に依存するウィジェットやプロバイダの更新が行われる。
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// `ref` を使ってプロバイダを監視する
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
ただし、onTap、onPressedなど、非同期な場面で使用してはならない。
watch メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。
ref.listen
プロバイダの値を監視し、値が変化するたび、関数を呼び出す(主に、画面遷移するとき、ダイアログを表示するときなど)。ref.listenには2つの引数が必要。
- 第1引数にプロバイダ
- 第2引数に状態が変化した際に実行する関数
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
});
ref.watchと同様、非同期な場面で用いてはならない。
listen メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。
ref.read
プロバイダの値を取得する(監視はしない)。クリックイベント等の発生時に、値を取得する場合に使用できる。公式では非推奨。
providerを使う際のウィジェット
通常のウィジェットでは ref を使用することができないため、ウィジェットをprovider用に書き換える必要がある。
StatelessWidgetを書き換える
StatelessWidget = ConsumerWidget と考えてもよい。
buildメソッドに第2パラメータ(WidgetRef ref)が存在する以外の違いはない。
StatefulWidgetを書き換える
StatefulWidget + State = ConsumerStatefulWidget + ConsumerState と考えてもよい。
実際に書き換えてみる
flutterで初期生成されるカウンタアプリをprovider用に書き換える。
デフォルト
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
StatefulWidgetとStateをまとめてConsumerWidgetに変更
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countProvider = StateProvider<int>((ref) => 0);
void main() {
runApp(ProviderScope(child: const MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends ConsumerWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
int _counter = 0;
void _incrementCounter() {
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
ref.watch(countProvider).toString(),
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.watch(countProvider.state).state++;
},
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
変更点
- MyHomePageをConsumerWidgetに変更
- Widget buildにWidgetRefの追加
- MyAppをproviderScopeで囲む
- カウンタを保存する変数_counterを、countProviderとして新たに定義
- カウンタの表示をcountProviderから取得
- onPressedでcountProvider.stateの値を書き換える
しかしこれだと、countProviderの値が変わるたびに、ウィジェットが更新されてしまう。
描画効率の面からもこれは避けたい。そこで、ウィジェットをstatelessWidgetに戻し、プロバイダのある箇所をConsumerで囲むことにする。これにより、画面全体ではなく、変更のあったConsumerの部分だけが再描画される。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countProvider = StateProvider<int>((ref) => 0);
void main() {
runApp(ProviderScope(child: const MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Consumer (
builder: (BuildContext context, WidgetRef ref ,Widget? child) =>
Text(
ref.watch(countProvider).toString(),
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: Consumer(
builder: (BuildContext context, WidgetRef ref ,Widget? child) =>
FloatingActionButton(
onPressed: () {
ref.watch(countProvider.state).state++;
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
変更点
- MyHomePageをstatelessWidgetに変更
- Widget buildからWidgetRefの削除
- 各プロバイダをConsumerで囲む
これでも十分だが、プロバイダのある場所をいちいちConsumerをで囲むのは面倒なのでConsumerStatefulWidgetを使用する。これにより、ウィジェット内で、Consumerで囲まずにrefを使用することができる。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countProvider = StateProvider<int>((ref) => 0);
void main() {
runApp(ProviderScope(child: const MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends ConsumerStatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
ConsumerState<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends ConsumerState<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
), Text(
ref.watch(countProvider).toString(),
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.watch(countProvider.state).state++;
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
変更点
- MyHomePageをConsumerStatefulWidgetに変更
- StateをConsumerStateに変更
- Consumerの削除
以上、カウンタアプリをConsumerWidget、ConsumerStatefulWidgetの2種類を用いて書き換えることに成功した。
終わりに
自分も初心者なので、誤り等あれば指摘していただけるとありがたいです。
最後まで読んでいただきありがとうございました。