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

> Flutterにおける状態管理のためのProviderとRiverpodの違いについて、今回の記事では「副作用のトリガーが簡単でない」を解決する方法について解説します。

- 公開日: 2024-10-24
- 更新日: 2024-11-30
- 著者: Matsu
- タグ: Flutter, Riverpod
- URL: https://tech.anycloud.co.jp/articles/provider-vs-riverpod-side-effect

---

## はじめに

<div class="link-card-wrap"><a class="link-card" href="/articles/provider-vs-riverpod-parameter/" target="_blank" rel="noopener noreferrer"><span class="link-card-body"><span class="link-card-title">【ProviderとRiverpodの違い⑥】信頼性のあるパラメータ化機構の欠如</span><span class="link-card-description">Flutterにおける状態管理のためのProviderとRiverpodの違いについて、今回の記事では「信頼性のあるパラメータ化機構の欠如」を解決する方法について解説します。</span><span class="link-card-meta"><img class="link-card-favicon" src="./linkcard-01-favicon.ico" alt=""><span class="link-card-domain">tech.anycloud.co.jp</span></span></span><img class="link-card-image" src="./linkcard-01-image.webp" alt=""></a></div>

前回に引き続き、今回もRiverpodのモチベーションについて解説していきます。

<div class="link-card-wrap"><a class="link-card" href="https://riverpod.dev/ja/docs/from_provider/motivation" target="_blank" rel="noopener noreferrer"><span class="link-card-body"><span class="link-card-title">モチベーション | Riverpod</span><span class="link-card-description">この記事は、なぜ Riverpod が存在するのかを詳しく説明することを目的としています。</span><span class="link-card-meta"><img class="link-card-favicon" src="./linkcard-02-favicon.svg" alt=""><span class="link-card-domain">riverpod.dev</span></span></span><img class="link-card-image" src="./linkcard-02-image.webp" alt=""></a></div>

今回の記事では、「[副作用のトリガーが簡単でない](https://riverpod.dev/ja/docs/from_provider/motivation#%E5%89%AF%E4%BD%9C%E7%94%A8%E3%81%AE%E3%83%88%E3%83%AA%E3%82%AC%E3%83%BC%E3%81%8C%E7%B0%A1%E5%8D%98%E3%81%A7%E3%81%AA%E3%81%84)」というセクションを掘り下げていきます。

## 副作用のトリガーが簡単でない

### 公式ドキュメントの説明

> `InheritedWidget`には`onChange`コールバックがないため、Provider にはそれがありません。
> 
> これは、スナックバーやモーダルなどのナビゲーションに問題を引き起こします。
> 
> 代わりに、Riverpod は単に`ref.listen`を提供し、Flutter とうまく統合します。

```dart
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のコンシューマーであるウィジェットは、ビルドメソッド内で条件を設定して監視し、変化があった場合にスナックバーを表示するなどの対応が必要になります。これは非常に冗長で、エラーを引き起こす可能性があります。

実際のコードで確認してみましょう。

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

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

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

<figure><img src="./image-001.webp" alt="Providerにおける副作用のトリガー問題" width="300" height="650"></figure>

## Riverpodにおける副作用の改善

Riverpodでは、`ref.listen`を使用して状態の変化を監視し、副作用をトリガーすることができます。

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