【ProviderとRiverpodの違い⑦】副作用のトリガーが簡単でない
はじめに
前回に引き続き、今回もRiverpodのモチベーションについて解説していきます。
今回の記事では、「副作用のトリガーが簡単でない」というセクションを掘り下げていきます。
副作用のトリガーが簡単でない
公式ドキュメントの説明
InheritedWidget
にはonChange
コールバックがないため、Provider にはそれがありません。これは、スナックバーやモーダルなどのナビゲーションに問題を引き起こします。
代わりに、Riverpod は単に
ref.listen
を提供し、Flutter とうまく統合します。
class DiceRollWidget extends ConsumerWidget {
const DiceRollWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(diceRollProvider, (previous, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Dice roll! We got: $next')),
);
});
return TextButton.icon(
onPressed: () => ref.invalidate(diceRollProvider),
icon: const Icon(Icons.casino),
label: const Text('Roll a dice'),
);
}
}
Providerにおける副作用のトリガー問題
ProviderはInheritedWidget
をベースにしていますが、InheritedWidget
には自身のデータが変更されたときにウィジェットに通知するonChange
コールバックがありません。
つまり、Providerもこの機能を直接提供していません。
これが、アプリ内でスナックバーやダイアログ、ナビゲーションのトリガーとなるような副作用を実装する際に問題が起きます。
たとえば、以下のようにProviderを使って何らかの値が変化したときにユーザーに通知する場合、Providerのコンシューマーであるウィジェットは、ビルドメソッド内で条件を設定して監視し、変化があった場合にスナックバーを表示するなどの対応が必要になります。これは非常に冗長で、エラーを引き起こす可能性があります。
実際のコードで確認してみましょう。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
class CounterModel with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int? lastCount;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Provider Side Effects')),
body: Center(
child: Consumer<CounterModel>(
builder: (context, model, child) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (lastCount != null && lastCount != model.count) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Count changed to ${model.count}'),
),
);
}
lastCount = model.count;
});
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${model.count}'),
ElevatedButton(
onPressed: () => model.increment(),
child: const Text('Increment'),
),
],
);
},
),
),
),
);
}
}
build
メソッド内で、カウントが変更されたかどうかを確認し、変更がある場合にはaddPostFrameCallback
を使ってスナックバーを表示します。
これはbuild
メソッドが完了した後にUIの更新をトリガーするために使用されています。
この方法において機能はしますが、副作用の管理においては、あまりエレガントではなく、エラーが発生しやすいことがあるでしょう。
また、副作用のトリガーが直感的でないため、より多くのボイラープレートコード(殆ど、または全く変化することなく、複数の場所で繰り返される定型コードのセクション)が必要になります。
Riverpodにおける副作用の改善
Riverpodでは、ref.listen
を使用して状態の変化を監視し、副作用をトリガーすることができます。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() {
state++;
}
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Riverpod Side Effects')),
body: const CounterApp(),
),
);
}
}
class CounterApp extends ConsumerWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (previous, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Count changed to $next')),
);
});
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${ref.watch(counterProvider)}'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Text('Increment'),
),
],
),
);
}
}
まとめ
Riverpodは、副作用のトリガーを簡単にし、エラーの可能性を減少させるような状態管理のアプローチを提供してくれます。
これにより、より直感的に状態の変更に応じたUIの反応を実装できるようになり、コードの可読性と保守性が向上します。