---
title: "Riverpod NotifierProviderの使い方とStateNotifierProviderとの違い｜3.4系の移行手順"
url: "https://www.issoh.co.jp/tech/details/2819/"
published: 2024-06-27
updated: 2026-10-27
categories: ["Flutter"]
publisher: "株式会社一創"
---

# Riverpod NotifierProviderの使い方とStateNotifierProviderとの違い｜3.4系の移行手順

Riverpodで画面の状態を持たせるとき、いまの標準は `NotifierProvider` です。かつての定番だった `StateNotifierProvider` は、Riverpod 3.0 で legacy（旧API）扱いになり、別の import に移されました。この記事では、NotifierProvider の書き方を最小のコードから示し、StateNotifierProvider との違い、既存コードを移すときに書き換える箇所、移行後に挙動が変わる点を説明します。動作は 2026年10月時点の最新版 riverpod 3.4.3 を Dart 3.13.5 で実行して確かめています。

## まとめ：NotifierProviderとStateNotifierProviderの違いと移行の結論

- **新しく書く状態は NotifierProvider（非同期なら AsyncNotifierProvider）で書きます。**StateNotifierProvider と StateProvider は 3.0 から `package:flutter_riverpod/legacy.dart` へ移され、新規に選ぶ理由はありません。
- **違いの中心は初期化の置き場所です。**Notifier は初期値と依存の `ref.watch` を `build()` に集め、`ref` はクラスのプロパティとして使えます。StateNotifier はコンストラクタの `super(初期値)` とプロバイダ定義に処理が分かれます。
- **既存の StateNotifier は legacy の import を足せばそのまま動きます。**非推奨の警告も出ないので、触る機会のあるクラスから1つずつ移せば足ります。
- **移行で挙動が変わる点が3つあります。**通知の判定が `identical` から `==` に変わる、`dispose()` と `mounted` が無くなる、引数は `FamilyNotifier` ではなくコンストラクタで受ける、の3点です。
- **await のあとに `ref.mounted` を見るだけでは、依存の変化による作り直しを検知できません。**Notifier は作り直しでも同じインスタンスのまま残るためで、対処は後半の章で示します。

## NotifierProviderの書き方：Notifierクラスから画面での参照まで

### build()で初期状態を返すNotifierクラス

`Notifier<T>` を継承し、`build()` の戻り値を初期状態にします。状態の変更は `state` への代入です。ほかのプロバイダを読むときは、クラスが持つ `ref` をそのまま使います。状態の初期化や依存の購読は build() に書き、familyの引数をフィールドに保持する処理はコンストラクタに書けます。

```
import 'package:flutter_riverpod/flutter_riverpod.dart';

class Counter extends Notifier<int> {
  @override
  int build() => 0; // 初期状態

  void increment() => state++;
  void reset() => state = 0;
}

// 型引数は <Notifierの型, 状態の型>。生成関数にはコンストラクタ参照を渡す
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
```

手書きの NotifierProvider は既定で自動破棄（autoDispose）されません。画面を離れたら状態を捨てたい場合は `NotifierProvider.autoDispose<Counter, int>(Counter.new)` と書きます。

### ref.watchとref.read(.notifier)の使い分け

画面側では、表示に使う値を `ref.watch(counterProvider)` で購読し、ボタンなどのイベントでは `ref.read(counterProvider.notifier)` で Notifier を取り出してメソッドを呼びます。`.notifier` を付けると状態の値ではなく Notifier インスタンスが返ります。`watch` はウィジェットのビルド中に購読を宣言するためのAPIなので、イベント処理では`read` にします。

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

class CounterView extends ConsumerWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Column(
      children: [
        Text('$count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: const Text('増やす'),
        ),
      ],
    );
  }
}
```

アプリ全体は `runApp(const ProviderScope(child: MyApp()))` のように ProviderScope で包みます。パッケージの導入から ProviderScope の配置までは、[Riverpodとは？Flutterの状態管理をRiverpod 3系で始める導入・使い方・移行ガイド](/tech/details/9959/)で手順を追っています。

## NotifierProviderとStateNotifierProviderの違い

どちらも「状態を持つクラスをプロバイダで公開する」仕組みで、画面側の `ref.watch(provider)` と `ref.read(provider.notifier)` の書き方は同じです。違うのはクラスの中身と、状態を変えたときの通知の条件です。

| 観点            | NotifierProvider       | StateNotifierProvider |
| ------------- | ---------------------- | --------------------- |
| 初期状態          | build() の戻り値           | super(初期値)            |
| 依存の ref.watch | build() の中             | プロバイダ定義の中             |
| ref の入手       | クラスのプロパティ              | コンストラクタ引数             |
| 後片付け          | ref.onDispose          | dispose() の上書き        |
| 通知の判定         | \==                    | !identical            |
| 引数付き          | コンストラクタで受ける            | .family の生成関数         |
| @riverpod 生成  | 対応                     | 非対応                   |
| 3.x の import  | flutter\_riverpod.dart | legacy.dart           |

### StateNotifierとNotifierの初期化・依存の配置

StateNotifier では、依存するプロバイダの `ref.watch` をプロバイダ定義に書き、その値と `ref` をコンストラクタへ渡し、初期化処理はコンストラクタに書く、という3か所に処理が散ります。公式の[From StateNotifier の移行ガイド](https://riverpod.dev/docs/migration/from%5Fstate%5Fnotifier)は、タイマーと後片付けを持つ例で「挙動を理解するのに8か所を見る必要がある」と書いています。Notifier ではこれが全部 `build()` に入るので、依存が変わったときに何が再実行されるかを1か所で読めます。

### 更新通知の判定：identicalから==へ

見落としやすいのが通知の条件です。StateNotifier の `updateShouldNotify` は `!identical(old, current)`（同じインスタンスでなければ通知）で、Riverpod 3.0 の Notifier はすべて `==`（等しくなければ通知）で判定します。中身の等しい新しいレコード `(name: 'a', age: 1)` を代入して通知回数を数えると、StateNotifierProvider は1回、NotifierProvider は0回でした。

freezed などで `==` を実装した状態クラスを使っていると、移行後は「同じ内容で作り直して再通知させる」書き方が効かなくなります。逆に、リストを `state.add(2)` で直接書き換えて `state = state` と再代入しても、同じインスタンスなので通知は0回です。`state = [...state, 3]` と新しいリストを作れば1回届きます。どうしても通知の条件を変えたいときは Notifier 側で `updateShouldNotify` を上書きします（`true` を返すようにすると、同じ値の代入でも1回通知されました）。

## StateNotifierProviderの3系での扱い：legacy.dartへの移動

[公式の3.0移行ガイド](https://riverpod.dev/docs/3.0%5Fmigration)は、StateProvider・StateNotifierProvider・ChangeNotifierProvider を legacy と位置づけ、使い続けるなら legacy 用の import に変えるよう案内しています。削除はされていません。

```
// Flutter アプリではこの1行に揃える
import 'package:flutter_riverpod/legacy.dart';

class CounterOld extends StateNotifier<int> {
  CounterOld() : super(0);
  void increment() => state++;
}

final counterOldProvider =
    StateNotifierProvider<CounterOld, int>((ref) => CounterOld());
```

import 先は2種類あり、中身が違います。[flutter\_riverpod 3.4.3 の legacy.dart](https://github.com/rrousselGit/riverpod/blob/flutter%5Friverpod-v3.4.3/packages/flutter%5Friverpod/lib/legacy.dart) は ChangeNotifierProvider を含みますが、riverpod 側の legacy.dart は StateNotifier 系と StateProvider だけです。Flutter アプリでは flutter\_riverpod 側を使います。

legacy の API には `@Deprecated` 注釈が付いていないため、`dart analyze` を通しても非推奨の警告は出ません。警告を頼りに移行対象を洗い出すことはできないので、`legacy.dart` を import しているファイルを検索して一覧にします。StateNotifier クラス自体は別パッケージ [state\_notifier](https://pub.dev/packages/state%5Fnotifier) のもので、その最新版 1.0.0 は2023年8月16日の公開から更新されていません。

StateProvider も同じく legacy です。「値を1つ持つだけ」の用途でも、`build()` で初期値を返し、変更用のメソッドを1つ持つ Notifier に置き換えます。

## StateNotifierからNotifierへ移行する手順

画面側のコードは変えずに済み、書き換えはクラスとプロバイダ定義に集まります。公式ガイドの論点を、書き換える順に並べます。

### 初期値と依存のbuild()への移動

`super(初期値)` を `build()` の戻り値にし、プロバイダ定義にあった `ref.watch` も `build()` に移します。コンストラクタ引数で受けていた `ref` は削除し、クラスの `ref` を使います。

```
// 移行前
class TodoListOld extends StateNotifier<List<String>> {
  TodoListOld(this.ref, this.limit) : super(const []);
  final Ref ref;
  final int limit;
}
final todoListOldProvider =
    StateNotifierProvider<TodoListOld, List<String>>((ref) {
  final limit = ref.watch(limitProvider);
  return TodoListOld(ref, limit);
});

// 移行後
class TodoList extends Notifier<List<String>> {
  @override
  List<String> build() {
    ref.watch(limitProvider); // 依存もここで読む。上限が変わると build() をやり直す
    return const [];
  }

  void add(String title) {
    if (state.length >= ref.read(limitProvider)) return;
    state = [...state, title]; // 新しいリストを代入する
  }
}
final todoListProvider =
    NotifierProvider<TodoList, List<String>>(TodoList.new);
```

### dispose()とmountedからref.onDispose・ref.mountedへの置き換え

Notifier には `dispose()` の上書きも `mounted` プロパティもありません。タイマーや購読の後片付けは、`build()` の中で `ref.onDispose(timer.cancel)` のように登録します。`build()` が再実行される前にも呼ばれるので、作り直しのたびに古いタイマーが止まります。`if (mounted)` は Riverpod 3.0 で追加された `ref.mounted` に置き換えます。autoDispose のプロバイダを await 中に破棄させると `ref.mounted` は `false` になり、確認せずに `state` へ代入した場合は `UnmountedRefException` が投げられました。

### 状態変更の監視：addListener・streamからlistenSelfへの移行

StateNotifier の `addListener` と `stream` で状態の変化を拾っていた処理は、Notifier の中なら `build()` で `listenSelf((previous, next) { ... })` を呼びます。実行すると、初期化時に `null -> 0`、更新時に `0 -> 1` が届きました。Notifier の外から監視する場合は、ほかのプロバイダと同じく `ref.listen` を使います。`debugState` は state\_notifier 1.0.0ですでに削除されています。Notifier移行後のテストの書き方は後述の章で示します。

### familyの引数を受け取るコンストラクタ

Riverpod 2系にあった `FamilyNotifier` は 3.0 で削除されました。引数は Notifier のコンストラクタで受け、`NotifierProvider.family` にコンストラクタ参照を渡します。

```
class TodoTitle extends Notifier<String> {
  TodoTitle(this.id); // 引数はコンストラクタで受ける
  final String id;

  @override
  String build() => 'todo-$id';
}

// 型引数は <Notifierの型, 状態の型, 引数の型>
final todoTitleProvider =
    NotifierProvider.autoDispose.family<TodoTitle, String, String>(TodoTitle.new);

// 使う側: ref.watch(todoTitleProvider('a'))
```

公式の [family のガイド](https://riverpod.dev/docs/concepts2/family)は、引数が変わったあとに古い状態が残り続けないよう、family には自動破棄を付けることを「highly advised」としています。引数ごとの状態を画面を離れても保持したい場合だけ `.autoDispose` を外します。`todoTitleProvider('a')` と `todoTitleProvider('b')` は別々の状態として保持され、実行結果は `todo-a` と `todo-b` でした。2系の `FamilyNotifier<int, String>` で `build(String arg)` と書いていたコードは、コンストラクタへの引数の移動と `build()` の引数の削除が要ります。

## Notifierのライフサイクル：再ビルド時のインスタンス保持

依存しているプロバイダの値が変わると `build()` が再実行されますが、Notifier のインスタンスは作り直されません。`build()` の中でフィールド `builds` を1ずつ増やす Notifier を用意し、依存の変更と `invalidate` を順に行うと、`identityHashCode` は3回とも同じで、`builds` は 1→2→3 と増え続けました。一方、`state` は `build()` の戻り値（0）に戻ります。

この挙動は開発版の途中で一度変わっています。[riverpod の変更履歴](https://pub.dev/packages/riverpod/changelog)では、3.0.0-dev.12 で「rebuild のたびに Notifier を作り直す」変更が入り、3.0.0-dev.16（2025年6月20日）で「Revert Notifier life-cycle change. They are once again preserved across rebuilds.」と差し戻されました。開発版の時期に書かれた解説には「作り直される」とあるものが残っているので、安定版の挙動として読まないでください。

実務で効いてくるのは、Notifier のフィールドに置いた値は `build()` の再実行では消えない、という点です。キャッシュや進行中フラグをフィールドに持たせるなら、`build()` の先頭で初期化し直すか、状態そのものに含めて `state` で管理します。

## await後のref.mountedが作り直しを検知しない落とし穴

前の章の性質から、もう1つの問題が出てきます。Notifier のメソッドで await している間に依存が変わって `build()` が再実行されると、メソッドの中の `ref` は新しいビルドの ref を指すため、`ref.mounted` は `true` を返します。次のコードで依存を変えた直後に await が明けると、`build()` が返した新しい状態（10）が、古い処理の書いた 99 で上書きされました。

```
class Profile extends Notifier<int> {
  @override
  int build() => ref.watch(depProvider) * 10;

  // 依存が変わっても ref.mounted は true のまま。新しい状態を 99 で上書きする
  Future<void> saveNaive() async {
    await Future.delayed(const Duration(milliseconds: 50));
    if (!ref.mounted) return;
    state = 99;
  }

  // await の前に ref を変数に取っておくと、作り直し後は false になる
  Future<void> save() async {
    final currentRef = ref;
    await Future.delayed(const Duration(milliseconds: 50));
    if (!currentRef.mounted) return; // 古いビルドの処理なので捨てる
    state = 99;
  }
}
```

`save()` の書き方では、await 前に取った ref と現在の ref が別物になり、`currentRef.mounted` は `false` でした。結果として状態は新しいビルドの 10 のまま残ります。変更履歴の 3.0.0-dev.12 には「When a provider is rebuilt, a new `Ref` is now created.」とあり、ビルドごとに ref が別物になるのは仕様です。作り直し後の古い ref が `true` を返していた不具合は 3.2.0 で直っているので、この書き方は 3.2.0 以降が前提です。

画面を閉じたときの破棄だけを気にするなら `ref.mounted` の確認で足ります。検索条件やログインユーザーのように、await 中に変わりうるプロバイダを `build()` で watch している Notifier では、await 前に ref を取る書き方にしてください。古い検索条件の結果が新しい条件の画面に出る、という不具合をこれで防げます。

## AsyncNotifierProviderで非同期の状態を持つ書き方

API取得のように読み込み中とエラーを持つ状態は、`AsyncNotifier` と `AsyncNotifierProvider` で書きます。`build()` の戻り値型は `FutureOr<T>` で、同期値または `Future<T>` を返せます。`state` は `AsyncValue<T>` になります。`build()` で投げた例外は自動で `AsyncError` になるので、公式ガイドが勧めるとおり `build()` には `try`／`catch` も `AsyncValue.guard` も書きません。

```
class Articles extends AsyncNotifier<List<String>> {
  @override
  Future<List<String>> build() async {
    return ref.watch(articleRepositoryProvider).fetchAll();
  }

  Future<void> add(String title) async {
    final previous = state.value;
    if (previous == null) return; // 初回の読み込みが終わるまでは追加しない
    final currentRef = ref;
    final repository = ref.read(articleRepositoryProvider);
    state = const AsyncLoading(); // 前回の一覧は value に残る
    final result = await AsyncValue.guard(() async {
      await repository.add(title);
      return [...previous, title];
    });
    if (!currentRef.mounted) return; // 待機中に作り直されたら結果を捨てる
    state = result;
  }
}

final articlesProvider =
    AsyncNotifierProvider<Articles, List<String>>(Articles.new);
```

`add()` では、前の章と同じく await の前に ref を取っておき、待機中にリポジトリが差し替わって `build()` が再実行されたら結果を捨てます。3.4.3 では `state = const AsyncLoading()` と代入しても前回の値は消えず、通知された状態は `AsyncLoading(value: [a])`（`hasValue` が true）でした。`guard` の中で例外が出た場合も、`AsyncError` の `value` には前回の一覧が残ります。エラーのときも直前の一覧を出したままにしたいなら、`when` の error 分岐に任せず、先に `value` の有無を見て一覧とエラー表示を併せて出します。

## Notifierのテスト：ProviderContainer.testとoverrideWithBuild

Notifier はコンストラクタで直接作るのではなく、プロバイダ経由で取り出します。手で `Counter()` と作ると `ref` が初期化されず使えません。3.0 で追加された `ProviderContainer.test()` は、テスト終了時にコンテナを自動で破棄します。`overrideWithBuild` を使うと、Notifier のメソッドは本物のまま、`build()` の初期値だけを差し替えられます。

```
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';

class Counter extends Notifier<int> {
  @override
  int build() => 0;
  void increment() => state++;
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);

void main() {
  test('increment で 1 増える', () {
    final container = ProviderContainer.test();
    container.read(counterProvider.notifier).increment();
    expect(container.read(counterProvider), 1);
  });

  test('build だけ差し替えて初期値 10 から始める', () {
    final container = ProviderContainer.test(
      overrides: [counterProvider.overrideWithBuild((ref, notifier) => 10)],
    );
    container.read(counterProvider.notifier).increment();
    expect(container.read(counterProvider), 11);
  });
}
```

この2件は、CounterとcounterProviderをテストファイル内に定義し、Riverpod本体を読み込む構成で `dart test` を実行して通過しました。Flutter のウィジェットテストでは `flutter_test` の `test` を使い、ウィジェットを ProviderScope で包みます。

## コード生成（@riverpod）で書くNotifier

riverpod\_generator を使うと、クラスに `@riverpod` を付けるだけでプロバイダの宣言が生成されます。2026年10月時点の組み合わせは riverpod\_annotation 4.0.7 と riverpod\_generator 4.0.9 です。

```
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'todo_title.g.dart';

@riverpod
class TodoTitle extends _$TodoTitle {
  @override
  String build(String id) => 'todo-$id'; // コード生成では引数を build に書く

  void rename(String title) => state = '$id: $title';
}
```

手書きの family と違い、コード生成では引数を `build()` に書きます。生成されたクラスには同名のゲッター `id` が作られ、メソッドからも読めます。生成されたプロバイダは既定で自動破棄が有効（`isAutoDispose` が true）で、手書きの既定と逆になる点に注意してください。`build_runner` の版の制約やエラーの切り分けは、[Riverpod 3系の導入ガイド](/tech/details/9959/)のコード生成の章にまとめています。

## 移行を急がなくてよい場面と先に移すべきクラス

legacy の StateNotifier は 3.4.3 でも動き、警告も出ません。全クラスを一度に書き換える必要はなく、次の条件に当たるものから移します。

- **await のあとに `mounted` を見ているクラス**：作り直しの扱いを前の章の書き方で整理する必要があるため、手を入れる価値が最も大きい部分です。
- **プロバイダ定義で `ref.watch` している StateNotifier**：依存が変わるたびにインスタンスごと作り直されていたので、Notifier に移すとフィールドの扱いが変わります。
- **永続化（experimental の persist）を使いたいクラス**：persist は Notifier 系の拡張なので StateNotifier からは呼べません。

逆に、テストが無く `==` を実装した状態クラスで再通知に頼っている StateNotifier は、先に通知回数を確かめるテストを書いてから移します。前に書いたとおり判定が `==` に変わると、同じ内容の再代入では画面が更新されなくなるためです。複数画面にまたがる状態設計から見直す場合は、[Flutter・React Nativeのアプリ開発支援](/service/apps/cross%5Fplatform/)で既存アプリの改修も扱っています。

## よくある質問

### NotifierProviderとは何ですか？

`Notifier` を継承したクラスの状態を画面に公開する Riverpod のプロバイダです。Riverpod 2.0 で導入され、3.0 からは同期的な状態を持つときの標準です。初期値は `build()` の戻り値で、変更は `state` への代入で行います。

### StateNotifierProviderはもう使えないのですか？

使えます。Riverpod 3.4.3 でも削除されておらず、`package:flutter_riverpod/legacy.dart` を import すれば動きます。`@Deprecated` の警告も出ません。新しく書くコードでは NotifierProvider を選びます。

### NotifierとAsyncNotifierの違いは何ですか？

Notifier は `build()` が値をそのまま返し、AsyncNotifier は同期値か `Future` を返し、`state` が `AsyncValue` になります。読み込み中やエラーの表示が要る状態は AsyncNotifier を選びます。

### StateProviderの代わりは何を使えばよいですか？

Notifier と NotifierProvider です。StateProvider も 3.0 で legacy になりました。値を1つ持つだけなら、`build()` で初期値を返し、`void set(int value) => state = value;` のようなメソッドを1つ持たせれば足ります。

### NotifierProviderに引数を渡すにはどうしますか？

`NotifierProvider.family<Notifierの型, 状態の型, 引数の型>` を使い、引数は Notifier のコンストラクタで受けます。3.0 で `FamilyNotifier` は削除されました。コード生成の場合は `build()` の引数に書きます。

## 関連記事

- [Riverpodとは？Flutterの状態管理をRiverpod 3系で始める導入・使い方・移行ガイド](/tech/details/9959/)
- [Riverpodと他の状態管理の違い｜Provider・Bloc・GetXとの比較と選び方【2026年10月】](/tech/details/7039/)
- [FlutterのMVVM実装をRiverpodで書く手順｜公式アーキテクチャガイド準拠のViewModel設計](/tech/details/4443/)
- [Flutterアーキテクチャのベストプラクティス｜公式ガイドのレイヤー設計とディレクトリ構成【Flutter 3.47対応】](/tech/details/7232/)
- [StatelessWidgetとStatefulWidgetの違いと使い分け｜Flutter 3.44の実装で確認する判定基準](/tech/details/4442/)

---

出典: [Riverpod NotifierProviderの使い方とStateNotifierProviderとの違い｜3.4系の移行手順](<https://www.issoh.co.jp/tech/details/2819/>)（株式会社一創）
