【Flutter/Dart/Riverpod】listen/listenManualメソッドの使い方!変更の監視と反映

【Flutter/Dart/Riverpod】listen/listenManualメソッドの使い方!変更の監視と反映

この記事からわかること

  • Flutter/Dart状態管理を行うRiverpodとは?
  • listen/listenManual使い方

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

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),
      ),
    );
  }
}

特徴

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;
  }
}

listenManualbuildではなくinitStateで登録します。listenManualは明示的にライフサイクルを管理する必要があるため、同じlistenManual何度も呼び出すとその回数分観測が登録されてしまいますController → Stateの同期にはcontroller.addListenerを使用しState → ControllerlistenManualを使用します。

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 入力通知

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index