【ProviderとRiverpodの違い⑦】副作用のトリガーが簡単でない

【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の更新をトリガーするために使用されています。

この方法において機能はしますが、副作用の管理においては、あまりエレガントではなく、エラーが発生しやすいことがあるでしょう。

また、副作用のトリガーが直感的でないため、より多くのボイラープレートコード(殆ど、または全く変化することなく、複数の場所で繰り返される定型コードのセクション)が必要になります。

Providerにおける副作用のトリガー問題

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の反応を実装できるようになり、コードの可読性と保守性が向上します。

Anycloudではプロダクト開発の支援を行っています

プロダクト開発をお考えの方はぜひAnycloudにご相談ください。

まずは相談する

記事を書いた人

Matsuura

エンジニア

Matsuura

Twitter

Anycloudでエンジニアしてます!主にFlutterやWebフロントをやっていて、最近はバックエンドやインフラに挑戦・苦戦中。