FlutterのRiverpodを使ってみる
はじめに
公式ドキュメントの「はじめに」と「コンセプト」を読む
下記を使う
- ConsumerWidget
- Provider
- StateProvider
- FutureProvider
- StreamProvider
- ProviderScope overrides
- Combining Provider States
HelloWorld
準備
flutter pub add flutter_riverpod
flutter pub get
コード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final helloWorldProvider = Provider((_) => 'Hello world');
void main() {
runApp(const ProviderScope(
child: MyApp(),
));
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final String value = ref.watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
child: Text(value),
),
),
);
}
}
実行結果
プロバイダとは
プロバイダはあるステート(状態)をラップするためのオブジェクトであり、その監視を可能にしてくれます。
プロバイダは完全にイミュータブル(不変)であり、関数をグローバルで宣言することと違いはありません。
refを使って他のプロバイダを利用したり、プロバイダのステートが破棄される際のコールバック関数を登録したりすることができます。
プロバイダを利用するには、Flutter アプリのルート(root)に ProviderScope を置く必要があります。
プロバイダの利用方法
プロバイダはすべて ref オブジェクトを引数として受け取ります。
final provider = Provider((ref) {
// `ref` を通じて他のプロバイダを利用する
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})
StatelessWidget の代わりに ConsumerWidget を継承する
StatefulWidget+State の代わりに ConsumerStatefulWidget+ConsumerState を継承する
ref は ConsumerState のプロパティです。
ref の使い道は主に3通りあります。
- ref.watch: 値を取得する+変化を監視する(変化したらウィジェットやプロバイダを更新する)
- ref.listen: 変化を監視する+コールバック関数を登録する(変化したら呼び出す)
- ref.read: 値を取得する
watch メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。これらの場合は代わりに ref.read を使用してください。
上記はlistenメソッドについても同様
【重要】 ref.read は build メソッドの中で使わない
下記のコードではref.watch
の代わりにref.read
を使っても効果は同等とのこと
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
なぜなのかは理解できない、もう少しドキュメントを読み進める必要がある
StreamProviderは下記3つの値の取得方法がある
- provider: 現在のステートを取得する
- provider.stream: Streamを取得する
- provider.future: Futureを取得する
コード例は下記の通り(引用)
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);
return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => const Text('Oops'),
data: (user) => Text(user.name),
);
}
Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}
Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}
つまり select を使って監視対象にするプロパティを返す関数を指定することができるのです。
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
プロバイダのステートを組み合わせる
プロバイダのコールバック関数に渡される ref オブジェクトの watch メソッドを使用してください。
final cityProvider = Provider((ref) => 'London');
final weatherProvider = FutureProvider((ref) async {
// `ref.watch` により他のプロバイダの値を取得・監視します。
// 利用するプロバイダ(ここでは cityProvider)を引数として渡します。
final city = ref.watch(cityProvider);
// 最後に `cityProvider` の値をもとに行った計算結果を返します。
return fetchWeather(city: city);
});
依存先のプロバイダの値が変わったら、その依存元のプロバイダはどうなりますか?
値の取得に watch を使っていれば、Riverpod は値の変化を検出して 自動的に 依存元のプロバイダの値を再評価してくれます。
依存するプロバイダの値が変わると自動で値が再評価されるプロバイダは Provider だけではありません。 すべてのプロバイダがこの性質を持ちます。
プロバイダ内で別のプロバイダの値を監視せずに、取得だけする方法はないですか?
このような場合は Repository オブジェクトに ref.read を渡してください。 そうすることで Repository オブジェクトは自身のタイミングで、別のプロバイダからトークンを取得できるようになります。
final userTokenProvider = StateProvider<String>((ref) => null);
final repositoryProvider = Provider((ref) => Repository(ref.read));
class Repository {
Repository(this.read);
/// `ref.read` 関数
final Reader read;
Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
read をプロパティに持つオブジェクトのテスト方法を教えてください。
これには ProviderContainer クラスを利用してください。
final repositoryProvider = Provider((ref) => Repository(ref.read));
test('fetches catalog', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
Repository repository = container.read(repositoryProvider);
await expectLater(
repository.fetchCatalog(),
completion(Catalog()),
);
});
プロバイダが頻繁に更新されます。どうしたらいいですか?
select を利用して必要なプロパティのみを監視
プロバイダ修飾子 .family
.family 修飾子の目的は「外部のパラメータをもとに一意のプロバイダを作成すること」です。
例えば family と FutureProvider を組み合わせることで、メッセージ ID に紐づく Message を取得することが可能です。
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});
次のように messagesFamily に引数を渡してください。
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}
引数として渡すオブジェクトの等価性
プリミティブ型(bool/int/double/String)か、定数(プロバイダ)もしくは == と hashCode をオーバーライドしたイミュータブル(不変)オブジェクトのいずれかが理想的です。
【重要】 オブジェクトが一定ではない場合は autoDispose 修飾子との併用が望ましい
こうしたメモリリークは .family と .autoDispose 修飾子を併用することで避けることができます。
family プロバイダに複数のパラメータを渡す
例えば以下のオブジェクトを渡すことでプロバイダに複数のパラメータを間接的に渡すことができます。
- tuple
- freezed
- built_value
- equitable
プロバイダ修飾子 .autoDispose
プロバイダ作成時に次のように .autoDispose 修飾子を付け加えてください。
final userProvider = StreamProvider.autoDispose<User>((ref) {
});
これで userProvider が参照されなくなった際に、ステートが自動的に破棄されるようになります。
プロバイダに .autoDispose 修飾子を付けると、ref オブジェクトに keepAlive というメソッドが追加されます。keepAlive メソッドを実行することで、プロバイダが参照されなくなった際にもステートを維持するよう Riverpod に伝えることができます。
使用例: プロバイダが参照されなくなったタイミングで HTTP リクエストをキャンセルする
final myProvider = FutureProvider.autoDispose((ref) async {
// http リクエストのキャンセルを実行するための package:dio のオブジェクト
final cancelToken = CancelToken();
// プロバイダのステートが破棄されたら http リクエストをキャンセル
ref.onDispose(() => cancelToken.cancel());
// データを取得しつつキャンセル用の `cancelToken` を渡す
final response = await dio.get('path', cancelToken: cancelToken);
// リクエストが成功したらステートを維持する
ref.keepAlive();
return response;
});
エラー:The argument type 'AutoDisposeProvider' can't be assigned to the parameter type 'AlwaysAliveProviderBase'
原因は .autoDispose 修飾子付きのプロバイダを、そうではないプロバイダ内で利用したためです。
ProviderObserver
ProviderObserver は ProviderContainer 内で起こる変化を監視します。ProviderObserver クラスを継承するクラスを定義し、使用したいメソッドをオーバーライドして使用してください。
didAddProvider はプロバイダが初期化されるたびに呼び出されます。公開される値は value パラメータで利用できます。
didDisposeProvider はプロバイダが破棄されるたびに呼び出されます。
didUpdateProvider はプロバイダが変更通知を送信するたびに呼び出されます。
ProviderObserverを使用するにはProviderScopeのobservers引数にリストとして渡す
ProviderScope(observers: [Logger()], child: const MyApp())
ConsumerWidget
コード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class User {
final int id;
final String name;
const User(this.id, this.name);
}
final userProvider = Provider<User>((_) => const User(1, "bob"));
void main() {
runApp(const ProviderScope(
child: MyApp(),
));
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final name = ref.watch(userProvider.select((value) => value.name));
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
child: Text(name),
),
),
);
}
}
実行結果
なんとDartPadでflutter_riverpodを使用できる
でもVSCodeを使うと快適すぎてDartPadに戻れなくなる
StateProvider
コード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countProvider = StateProvider<int>((ref) => 0);
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(countProvider);
return Scaffold(
appBar: AppBar(title: const Text('AppBar Title')),
body: Center(
child: Text("Count: $count"),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
ref.read(countProvider.notifier).state = count + 1;
// or
// ref.read(countProvider.notifier).update((count) => count + 1);
},
icon: const Icon(Icons.plus_one),
label: const Text('Increment'),
),
);
}
}
実行結果
FutreProvider
コード例
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final nameProvider = FutureProvider<String>((ref) async {
await Future.delayed(const Duration(seconds: 3));
return 'bob';
});
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final name = ref.watch(nameProvider);
return Scaffold(
appBar: AppBar(title: const Text('AppBar Title')),
body: Center(
child: name.when(
data: (name) => Text("Name: $name"),
error: (err, stack) => Text("Error: $err"),
loading: () => const CircularProgressIndicator(),
),
),
);
}
}
実行結果
StreamProvider
コード例
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countProvider = StreamProvider<int>((ref) async* {
for (var i = 3; i >= 0; i -= 1) {
yield i;
await Future.delayed(const Duration(seconds: 1));
}
});
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(countProvider);
return Scaffold(
appBar: AppBar(title: const Text('AppBar Title')),
body: Center(
child: count.when(
data: (data) => Text("Count: $data"),
error: (err, stack) => Text("Error: $err"),
loading: () => const CircularProgressIndicator(),
),
),
);
}
}
実行結果
ProviderScope overrides
コード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final nameProvider = Provider((ref) => "Bob");
void main() {
runApp(
ProviderScope(
overrides: [
nameProvider.overrideWithValue("Alice"),
],
child: const MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final name = ref.watch(nameProvider);
return Scaffold(
appBar: AppBar(title: const Text('AppBar Title')),
body: Center(
child: Text("My name is $name."),
),
);
}
}
実行結果
Combining Provider States
コード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final nameProvider = StateProvider((ref) => "Bob");
final greetProvider = Provider(((ref) {
final name = ref.watch(nameProvider);
return "My name is $name";
}));
final buttonLabelProvider = StateProvider((ref) {
final name = ref.watch(nameProvider);
if (name == "Alice") {
return "Bob";
} else {
return "Alice";
}
});
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final greet = ref.watch(greetProvider);
final buttonLabel = ref.watch(buttonLabelProvider);
return Scaffold(
appBar: AppBar(title: const Text('AppBar Title')),
body: Center(
child: Text(greet),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
ref.read(nameProvider.notifier).state = buttonLabel;
},
label: Text(buttonLabel),
),
);
}
}
実行結果
StateNotifierProvider
コード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Todo {
final String title;
const Todo(this.title);
}
class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);
void addTodo(Todo todo) {
state = [...state, todo];
}
void removeTodo(Todo removedTodo) {
state = [
for (final todo in state)
if (todo != removedTodo) todo
];
}
}
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todosProvider);
return Scaffold(
appBar: AppBar(title: const Text('AppBar Title')),
body: ListView(
children: [
for (final todo in todos)
ListTile(
onTap: () {
ref.read(todosProvider.notifier).removeTodo(todo);
},
title: Text(todo.title),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// ignore: prefer_const_constructors
ref.read(todosProvider.notifier).addTodo(Todo("Todo Title"));
},
child: const Icon(Icons.add),
),
);
}
}
実行結果
ChangeNotifierProvider
原則として使用は非推奨
コード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Todo {
final String title;
const Todo(this.title);
}
class TodosNotifier extends ChangeNotifier {
final todos = <Todo>[];
void addTodo(Todo todo) {
todos.add(todo);
notifyListeners();
}
void removeTodo(Todo todo) {
todos.remove(todo);
notifyListeners();
}
}
final todosProvider = ChangeNotifierProvider<TodosNotifier>((ref) {
return TodosNotifier();
});
void main() {
runApp(
const ProviderScope(
child: MaterialApp(
home: MyApp(),
),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todosProvider).todos;
return Scaffold(
appBar: AppBar(title: const Text('AppBar Title')),
body: ListView(
children: [
for (final todo in todos)
ListTile(
onTap: () {
ref.read(todosProvider.notifier).removeTodo(todo);
},
title: Text(todo.title),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// ignore: prefer_const_constructors
ref.read(todosProvider.notifier).addTodo(Todo("Todo Title"));
},
child: const Icon(Icons.add),
),
);
}
}
実行結果
おまけ:Provider
準備
flutter create hello_provider
cd hello_provider
flutter pub add provider
flutter pub get
コード
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: const MaterialApp(
home: MyApp(),
)
)
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
final count = context.watch<Counter>().count;
return Scaffold(
appBar: AppBar(title: const Text('AppBar Title')),
body: Center(
child: Text("Count: $count"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<Counter>().increment();
},
child: const Icon(Icons.add),
),
);
}
}
実行結果
以上で一旦クローズ、次はREST APIクライアントを使ってみる