13 人が閲覧(直近 30 日) Flutter

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

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系で始める導入・使い方・移行ガイドで手順を追っています。

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 の移行ガイドは、タイマーと後片付けを持つ例で「挙動を理解するのに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移行ガイドは、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 は ChangeNotifierProvider を含みますが、riverpod 側の legacy.dart は StateNotifier 系と StateProvider だけです。Flutter アプリでは flutter_riverpod 側を使います。

legacy の API には @Deprecated 注釈が付いていないため、dart analyze を通しても非推奨の警告は出ません。警告を頼りに移行対象を洗い出すことはできないので、legacy.dart を import しているファイルを検索して一覧にします。StateNotifier クラス自体は別パッケージ state_notifier のもので、その最新版 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 のガイドは、引数が変わったあとに古い状態が残り続けないよう、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 の変更履歴では、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系の導入ガイドのコード生成の章にまとめています。

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

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

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

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

よくある質問

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() の引数に書きます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

RELATED POSTS 関連記事

目次