【Flutter/Dart/Riverpod】listen/listenManualメソッドの使い方!変更の監視と反映
この記事からわかること
- Flutter/Dartで状態管理を行うRiverpodとは?
- listen/listenManualの使い方
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Otter 2 Feature Drop
- Android OS:15以降
- Kotlin:2.2.21
- Xcode:26.0.1
- iOS:26
- Swift:6
- Flutter:3.38.3
- Dart:3.10.1
- Riverpod:2.6.1
- macOS(M1):Tahoe 26.0.1
Riverpodで変更をWidget側から明示的に監視して副作用を実行する
RiverpodのProviderが持っている値変更を監視する方法はwatchがあるかなと思いますが、これはUIに対して変更を通知して再描画させるための役割になっています。変化を検知した際に副作用を実行したい場合はwatchでは実現することができません。
変化を検知した際に副作用を実行したい場合はlisten/listenManualメソッドを使用します。
watchとの違い
| 項目 | watch | listen |
|---|---|---|
| 役割 | UIを再描画 | 副作用を実行 |
| buildの再実行 | される | されない |
| 使う場所 | buildメソッド | build/initState |
| 返り値 | Providerのstate | なし |
listenメソッドの使い方
void listen<T>(
ProviderListenable<T> provider,
void Function(T? previous, T next) listener, {
void Function(Object error, StackTrace stackTrace)? onError,
});
listenメソッドはRefから呼び出します。ジェネリクスでProviderのStateの型を指定し、第一引数には対象のProviderを渡します。listenerの引数からpreviousからは1つ前のStateが、nextから更新されたStateが取得することができます。実際に使用する際にはbuildの中で呼び出せばOKです。観測はRefが破棄されたタイミングで自動破棄され、再buildされる時も重複してすでにlisten済みであれば登録されることはありません。
例として以下のようなProviderがあると仮定して実装方法をみていきます。
final counterProvider =
StateNotifierProvider<CounterNotifier, int> (
(ref) => CounterNotifier(),
);
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() {
state++;
}
}
class CounterView extends ConsumerWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ① UI 表示用(watch)
final count = ref.watch(counterProvider);
// ② 副作用用(listen)
ref.listen<int> (counterProvider, (prev, next) {
if (prev != next && next == 5) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('5になりました 🎉')),
);
}
});
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Text(
'$count',
style: const TextStyle(fontSize: 40),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: const Icon(Icons.add),
),
);
}
}
特徴
- Refが破棄されたタイミングで自動破棄
- build内で使用
- 再buildでも重複登録されない
- 目的はUI副作用(SnackBar/Dialog/Navigator/etc..)
listenManualメソッドの使い方
ProviderSubscription<T> listenManual<T>(
ProviderListenable<T> provider,
void Function(T? previous, T next) listener, {
void Function(Object error, StackTrace stackTrace)? onError,
bool fireImmediately,
});
listenManualメソッドも基本的にはlistenと変わりません。大きく異なるのは明示的に生存期間を管理できることです。listenはbuildのライフサイクルに依存する形で自動で管理されていましたが、listenManualは返り値にProviderSubscription型が返るため、不要になったタイミングで明示的にcloseで破棄しておかないとメモリリークを引き起こす可能性を孕んでいます。
listenManualの役割はUI副作用というよりControllerとの同期で活用できます。TextEditingControllerの変化の値をViewModelのStateと常に同期させたい場合を例に見てみます。
final nameProvider =
StateNotifierProvider<NameNotifier, String> ((ref) {
return NameNotifier();
});
class NameNotifier extends StateNotifier<String> {
NameNotifier() : super('');
void update(String value) {
state = value;
}
}
listenManualはbuildではなくinitStateで登録します。listenManualは明示的にライフサイクルを管理する必要があるため、同じlistenManualを何度も呼び出すとその回数分観測が登録されてしまいます。Controller → Stateの同期にはcontroller.addListenerを使用しState → ControllerにlistenManualを使用します。
class NameInputView extends ConsumerStatefulWidget {
const NameInputView({super.key});
@override
ConsumerState<NameInputView> createState() => _NameInputViewState();
}
class _NameInputViewState extends ConsumerState<NameInputView> {
final _controller = TextEditingController();
late final ProviderSubscription<String> _subscription;
@override
void initState() {
super.initState();
_listenControllerToState();
_listenStateToController();
}
/// Controller → State
void _listenControllerToState() {
final notifier = ref.read(nameProvider.notifier);
_controller.addListener(() {
final text = _controller.text;
if (text != ref.read(nameProvider)) {
notifier.update(text);
}
});
}
/// State → Controller
void _listenStateToController() {
_subscription = ref.listenManual<String> (
nameProvider,
// Stateの初期値でも処理を実行する
fireImmediately: true,
(prev, next) {
if (_controller.text == next) return;
// build外で安全に反映
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.text = next;
});
},
);
}
@override
void dispose() {
_subscription.close();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: const InputDecoration(labelText: '名前'),
);
}
}
またlistenManualは購読を開始したタイミングでは処理を実行しません。Stateの初期値でも処理を実行したい場合はfireImmediately: trueを指定しておけば初回でも処理が動作するようになります。また相互の同期処理が無限に繰り返されないようにWidgetsBinding.instance.addPostFrameCallbackでbuild中に変更処理が走らないように制御しておきます。
それぞれの使い分けまとめ
| 役割 | 手段 |
|---|---|
| ref.watch | UI描画 |
| ref.listen | UI副作用 |
| ref.listenManual | Controller同期 |
| controller.addListener | 入力通知 |
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。





