【Flutter】Providerパッケージの基本を深掘りしてみた

# 投稿者について
2024/09からFlutter開発に携わっているモバイルエンジニアです。記事内容について誤りがありましたら、ご指摘いただけると幸いです。

# はじめに
自分が携わっているFlutterプロジェクトでは`Riverpod`の`StateNotifierProvider`をよく使用するのですが、「そもそもProviderって何だろう？」と思ったのが、本記事を書くきっかけとなりました。

本記事は、`StateNotifierProvider`の基礎となる`Provider`について深掘りし、自分なりに整理した内容となっています。


# 状態管理って何？

## 状態管理の基本的な考え方

Flutterでいう「状態」とは、アプリ内で変化するデータやUIの状態を指します。例えば、次のようなものが挙げられます。

- ボタンを押した回数（カウンター）
- ログインしているユーザーの情報
- APIから取得したデータ
- フォームの入力値

これらの状態を適切に管理することが「状態管理」です。  
Flutterでは、**UI（ウィジェットツリー）と状態が密接に結びついている**ため、状態が変化するとUIも再描画されます。

# 2つの状態管理の方法

状態管理の中でも大きく分けて2種類あります。

(1) ウィジェットの中だけで保持する状態
(2) アプリ全体で保持する状態

(1) の場合、画面を閉じたり別のページに移動したりして、そのページが**ウィジェットツリーから削除されたら**状態はリセットされます。

(2) の場合、アプリ全体で持っているので、**画面遷移が起きても毎回リセットされることはありません**。

言い換えれば、管理される状態は主に
(1)ウィジェットに依存する状態
(2)ウィジェットに依存しない状態
の2つになると考えられます。

# Providerって何？

## Providerの概要

公式ドキュメント：[provider | Flutter package](https://pub.dev/packages/provider)

**Provider** は、Flutterの状態管理パッケージの1つです。

先ほど2種類の状態管理について説明しましたが、Providerは **(2)ウィジェットに依存しない状態管理** に適しています。詳しく解説する前に、まず使い方を見ていきましょう。

### Providerの使用手順

Providerは、以下の3つの手順で使用することができます。

1. **状態クラスを作成**  
   →状態のデータと状態を操作する処理を作成

2. **状態管理を行う範囲にプロバイダーを設定**  
   ウィジェットツリーの中でデータを必要とする部分に提供

3. **状態の変化を通知**  
   状態が変更された場合に、それを監視しているウィジェットを再描画

## 実際に使用してみよう

### 必要なパッケージをインストール

まずは `provider` パッケージをプロジェクトに追加します。
````
flutter pub add provider
````

以下の例では、カウンターアプリを作成します。

### 1. 状態クラスを作成
ChangeNotifier を継承した状態クラスを定義します。

````dart:counter.dart
import 'package:flutter/material.dart';

/// カウントを管理するクラス
class Counter extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners(); // 状態の変更を通知
  }
}
````

### 2. Providerをセットアップ
main.dart にProviderを設定します。

````dart:main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';
import 'counter_screen.dart';

void main() {
  runApp(
    /// Providerを設定
    ChangeNotifierProvider(
      create: (_) => Counter(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterScreen(),
    );
  }
}

````

### 3. 状態を利用するウィジェット
Consumer を使って状態を監視し、UIを更新します。

````dart:counter_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';

/// カウンター画面
class CounterScreen extends StatelessWidget {
  const CounterScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          /// カウント数を表示する
          '${context.watch<Counter>().count}',
        ),
      ),

      /// ボタン
      floatingActionButton: FloatingActionButton(
        /// ボタン押下時にカウントする
        onPressed: () => context.read<Counter>().increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}
````

## 一個一個見てみよう

### 1. 状態クラス

````dart:counter.dart
import 'package:flutter/material.dart';

/// カウントを管理するクラス
class Counter extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners(); // 状態の変更を通知
  }
}
````

#### ChangeNotifierクラスとは
公式ドキュメント：[ChangeNotifier class](https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html)

このクラスはProviderのパッケージ内にあるクラスではなく、flutterのmaterialパッケージに存在するクラスです。

主に以下２つの操作が可能です。

1. **リスナーを登録・解除する**
他のウィジェットやオブジェクトが、このクラスの状態変更を監視できるようにする（`addListener`, `removeListner`）

2. **状態の変更を通知する**
`notifyListeners()` メソッドを呼び出すことで、登録されたリスナーに状態変更を通知する

`notifyListeners()`は状態クラスのコード内にもありますね。カウントした結果を通知してくれる役割を持っています。

#### ちょっと深掘り

##### 1. リスナーって何？
- `StatefulWidget`の状態や他のオブジェクトで、特定のイベントが発生したときに再描画や再計算などを実行するためのコールバックだよ！
- `ChangeNotifier`クラスには、リスナーを格納する内部リストがあって、このリストは、`addListener`を使って登録されたリスナーを保持して、`removeListener`で削除されるよ！

##### 2. 通知って具体的に何？

`notifyListeners()`が呼ばれると、以下の流れで通知が行われるよ！

1. 内部リストに登録されているリスナーを順番に取得
2. 各リスナー関数を実行


### 2. Providerのセットアップ

````dart:main.dart
import 'package:flutter/material.dart';

/// カウントを管理するクラス
class Counter extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners(); // 状態の変更を通知
  }
}
````

### 2. Providerをセットアップ
main.dart にProviderを設定します。

````dart:main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';
import 'counter_screen.dart';

void main() {
  runApp(
    /// Providerを設定
    ChangeNotifierProvider(
      create: (_) => Counter(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterScreen(),
    );
  }
}

````

#### ChangeNotifierProviderとは

公式ドキュメント：[ChangeNotifierProvider\<T extends ChangeNotifier?> class](https://pub.dev/documentation/provider/latest/provider/ChangeNotifierProvider-class.html)

ここからproviderパッケージのクラスになります。
`ChangeNotifierProvider`は、上記の通り、**ChangeNotifierクラスの拡張クラス**です。

`runApp` > `ChangeNotifierProvider` > `MyApp` の順で設置されており、MyApp全体の状態変化イベントを監視していることが分かります。

### 3. 状態を利用するウィジェット
Consumer を使って状態を監視し、UIを更新します。

````dart:counter_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';

/// カウンター画面
class CounterScreen extends StatelessWidget {
  const CounterScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          /// カウント数を表示する
          '${context.watch<Counter>().count}',
        ),
      ),

      /// ボタン
      floatingActionButton: FloatingActionButton(
        /// ボタン押下時にカウントする
        onPressed: () => context.read<Counter>().increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}
````

注目するのは以下2つです。いずれも`Provider`パッケージのクラスになります。

`Riverpod`でもかなりお世話になっているメソッドなので、本記事で一番重要な内容かもしれません。

```dart
/// カウント数を表示する
context.watch<Counter>().count
```

````dart
/// ボタン押下時にカウントする
onPressed: () => context.read<Counter>().increment()
````

#### context.watchとは

[watch<T> method](https://pub.dev/documentation/provider/latest/provider/WatchContext/watch.html)

`Provider`から値を取得し、その値が変更されると**ウィジェットを自動的に再ビルド**するメソッドです。

今回の場合でいうと、カウンターの数が変化したら自動的に表示が変わる=ウィジェットが再ビルドされているということになります。

#### context.readとは

[read<T> method](https://pub.dev/documentation/provider/latest/provider/ReadContext/read.html)

`Provider`から値を取得し、取得した値の変更によって**ウィジェットが再ビルドされない**（context.watchと逆）メソッドです。

カウント数を加算する際、カウンタークラスのデータは取得したいけどウィジェットの再ビルドは必要ないのでreadを使用しているということになります。

#### (ついでに説明)context.select

[select<T, R> method](https://pub.dev/documentation/provider/latest/provider/SelectContext/select.html)

オブジェクト全体ではなく、その一部にのみ依存する場合に使用するのが`context.select`です。
````dart
Widget build(BuildContext context) {
  final name = context.select((Person person) => person.name);
  return Text(name);
}
````

`context.watch` は値全体を監視してウィジェットを再ビルドするのに対し、`context.select` は指定された部分 (selector で取得した部分) の変更にのみ反応します。

## 余談

### 複数のProviderを使用する場合

先ほどのコードだと一個のProviderしか使用できない構成でした。
もし複数Providerをセットする場合は`MultiProvider`を使用します。

公式ドキュメント：[MultiProvider class - provider library - Dart API
](https://pub.dev/documentation/provider/latest/provider/MultiProvider-class.html)

````dart:main.dart
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: const MyApp(),
    ),
  );
}
````

### Consumerクラス

公式ドキュメント：[Consumer class - provider library - Dart API](https://pub.dev/documentation/provider/latest/provider/Consumer-class.html)



`context.watch<Counter>~`の部分ですが、以下の書き方でもできるそうです。

````dart
Consumer<Counter>(
  builder: (context, counter, child) {
    /// カウント時、カウント数が自動的に変化する
    return CenterText('カウント: ${counter.count}'),
    );
  },
),
````
`Consumer`クラスは、`Provider`を使用してデータを管理する際に、特定のウィジェットの再構築を制御するWidgetクラスです。


- `Provider`=提供者=**データを管理し提供するクラス**
- `Consumer`=消費者=**データを受け取り使用するクラス**

という意味合いでクラス名が使われていそうです。
`ConsumerWidget`や`HookConsumerWidget`などもここから取ってきていそうですね。

# まとめ

普段何気なく`Riverpod`を使用していますが、`Provider`を基に構築されたクラスやメソッドが多く、`Provider`の理解が`Riverpod`の理解にも繋がると感じました。

本記事が、`Provider`の理解や状態管理の入門としてお役に立てば幸いです。