Riverpodは、Flutterアプリの状態と依存オブジェクトをウィジェットツリーの外で定義し、画面から購読させるライブラリです。作者はproviderパッケージと同じRemi Rousselet氏で、pub.devのREADMEは自らを「anagram of Provider」(Providerの並べ替え)と紹介し、役割を「A reactive caching and data-binding framework」と書いています。2025年9月10日に3.0.0が安定版になり、2026年10月2日時点の最新は3.4.3です。
この記事は「Riverpodを選ぶべきか、ほかの状態管理を選ぶべきか」を決めるための比較に絞ります。比較対象はProvider・Bloc・GetX・Redux・MobX・get_itで、版やダウンロード数はpub.devのAPIから、挙動はFlutter 3.47.5で実際にコードを動かして確かめた結果だけを載せています。Riverpod 3系の導入手順やコード生成の書き方はRiverpod 3系の導入・コード生成・移行ガイドで扱っています。
まとめ:Riverpodを選ぶ条件と他を選ぶ条件
新規のFlutterアプリで、API呼び出しなどの非同期データが画面の大半を占めるなら、第一候補はRiverpodです。ローディングとエラーの状態をAsyncValueで自動的に持てるうえ、依存性注入(DI)も同じ仕組みで済みます。一方、次の条件に当てはまる場合は別の選択肢のほうが合います。
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 新規開発・非同期取得が多い | Riverpod | AsyncValueとDIが標準 |
| Providerで安定稼働中 | Provider継続 | 移行は1つずつ後からできる |
| 状態遷移の履歴を追いたい | Bloc | onTransitionでイベントを記録 |
| 1画面で閉じる一時的な状態 | setState | ライブラリ不要 |
| DIだけが欲しい | get_it | 状態管理を持ち込まない |
| 新規でGetXを検討中 | 他を選ぶ | 5.0が4年以上プレリリース |
Riverpod 3.4.1以降はDart 3.12以上(Flutter 3.44.0以降)を要求します。Flutterの版を上げられない案件では、Riverpodを選ぶ前にSDK要件を確認してください。
Riverpodの立ち位置:Providerの作者が作り直した状態管理とDI
Providerは、READMEの冒頭で「A wrapper around [InheritedWidget]」と説明されているとおり、Flutter標準のInheritedWidgetを扱いやすくする薄い層です。値はウィジェットツリーに置かれ、BuildContextから型で探して取り出します。
Riverpodは、この「ツリーに置いて型で探す」部分をやめました。Providerはトップレベルの変数として宣言し、値はアプリのルートに置いたProviderScopeの中のコンテナが保持します。画面は変数そのものを指定して読むので、探し方が型ではなく参照になります。READMEが利点として最初に挙げているのは「Handling errors/loading states by default」、つまり非同期処理のローディングとエラーを既定で扱える点です。
両パッケージはpub.devでも同じ発行者(dash-overflow.net)から公開されています。Riverpodは「Providerの次の版」ではなく別パッケージで、Provider自体も2025年8月に6.1.5+1が出ており、保守は続いています。
主要な状態管理パッケージの現況(2026年10月2日時点)
2026年10月2日にpub.devの公開API(/api/packages/パッケージ名と/score)から取得した値です。公開日はUTC基準です。いいね数は累計、ダウンロード数は直近30日分なので、「長く使われてきたか」と「いま使われているか」は別の列で読んでください。
| パッケージ | 最新安定版 | 公開日 | いいね | 30日DL |
|---|---|---|---|---|
| flutter_riverpod | 3.4.3 | 2026-09-03 | 2,910 | 約332万 |
| provider | 6.1.5+1 | 2025-08-19 | 11,008 | 約112万 |
| flutter_bloc | 9.1.1 | 2025-05-02 | 8,081 | 約197万 |
| get(GetX) | 4.7.3 | 2025-11-24 | 15,605 | 約78万 |
| get_it | 9.3.0 | 2026-09-19 | 4,725 | 約210万 |
| flutter_mobx | 2.4.0 | 2026-09-11 | 719 | 約10万 |
| flutter_redux | 0.10.0 | 2022-05-14 | 571 | 約5万 |
この表から読み取れる事実は3つです。
- いいね数が最も多いのはGetXですが、直近30日のダウンロード数はRiverpodの4分の1以下です。累計のいいね数と直近30日のダウンロード数は集計対象が異なるため、この順位の違いだけで現在の人気や採用数は断定できません。
flutter_blocは依存パッケージにprovider: ^6.0.0を持っています。BlocProviderによる受け渡しは、内部でProviderの仕組みを使っています。内部実装の依存関係とは別に、アプリの状態更新方式としてはBlocとProvider+ChangeNotifierのどちらかを選ぶ関係です。flutter_reduxの最終公開は2022年5月です。Dart 3.13の環境でも依存解決は通りますが、4年以上更新がありません。
同じカウンターを3つのライブラリで書いたコードの違い
ボタンを押すと数字が1増える画面を、Riverpod・Provider・Blocで書きました。掲載するのは画面と状態クラスの部分で、起動時はRiverpod版ならProviderScope、Provider版ならChangeNotifierProvider(create: (_) => Counter())、Bloc版ならBlocProvider(create: (_) => CounterBloc())で画面を包みます。3本ともflutter analyzeで警告ゼロ、ウィジェットテストでタップ後に「1」が表示されることを確認しています。
Riverpodでは、状態を持つNotifierとそれを公開するNotifierProviderをトップレベルに置き、画面はConsumerWidgetのrefで読みます。アプリのルートをProviderScopeで包むのが前提です。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final counterProvider =
NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class RiverpodCounterPage extends ConsumerWidget {
const RiverpodCounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}
Providerでは、ChangeNotifierを継承したクラスをChangeNotifierProviderでツリーに置き、context.watchとcontext.readで取り出します。値を変えたあとにnotifyListeners()を呼ぶのは書き手の責任です。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Counter extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class ProviderCounterPage extends StatelessWidget {
const ProviderCounterPage({super.key});
@override
Widget build(BuildContext context) {
final count = context.watch<Counter>().count;
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<Counter>().increment(),
child: const Icon(Icons.add),
),
);
}
}
Blocでは、イベントの型を定義し、on<Incremented>で「どのイベントが来たら、どの状態を出すか」を登録します。画面はメソッドを直接呼ばず、addでイベントを送ります。
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
sealed class CounterEvent {}
final class Incremented extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Incremented>((event, emit) => emit(state + 1));
}
}
class BlocCounterPage extends StatelessWidget {
const BlocCounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) => Text('$count'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Incremented()),
child: const Icon(Icons.add),
),
);
}
}
| 観点 | Riverpod | Provider | Bloc |
|---|---|---|---|
| 状態の置き場所 | ProviderScopeのコンテナ | ウィジェットツリー | ウィジェットツリー |
| 画面からの読み方 | ref.watch(変数) | context.watch<型>() | BlocBuilder<型, 状態> |
| 更新の入口 | メソッド呼び出し | メソッド呼び出し | イベント送信 |
| 再描画の通知 | state代入で自動 | notifyListeners() | emitで自動 |
| 空行を除く行数 | 23行 | 24行 | 26行 |
カウンター程度では行数の差はほとんど出ません。差が開くのは、非同期処理・依存の差し替え・テストを足したときです。以下の章でその3点を比べます。
RiverpodとProviderの違い
依存の探し方:BuildContextの型検索とトップレベル変数
上のProvider版の画面を、ChangeNotifierProviderで包まずに表示すると、ProviderNotFoundExceptionが発生し「Could not find the correct Provider<Counter> above this ProviderCounterPage Widget」と表示されます。Providerは型でツリーを上にたどるため、置き忘れや置く位置の間違いは、その画面を開くまで見つかりません。
Riverpod版をProviderScopeなしで表示した場合も実行時エラーになり、StateErrorで「No ProviderScope found」と出ます。ただしProviderScopeはアプリのルートに1つ置けば済むので、確認する場所は1か所です。個々のProviderは変数として参照するため、存在しないProviderを読むコードはそもそもコンパイルが通りません。Riverpod公式の移行の動機を説明したページもProviderNotFoundExceptionについて「Riverpod simply can’t throw this exception.」と書いています。
同じ型の値の併用:型検索とProvider参照の違い
Providerは型で値を探すため、同じ型を2つ重ねて置くと、内側の1つしか取り出せません。Provider<String>を外側に「outer」、内側に「inner」で重ねてcontext.read<String>()を呼ぶと、返るのは「inner」だけでした。外側の値を読むには、Stringを包む専用クラスを作って型を分ける必要があります。
Riverpodは変数で区別するので、この制約がありません。公式ドキュメントは2つのProviderがどちらもStringを返す例を示し、「The fact that both providers create a String does not cause conflicts.」と説明しています。APIのベースURLと認証トークンのように、型は同じでも意味が違う値を並べて扱えます。
Providerからの段階移行:1つずつ置き換える手順
Riverpod公式の移行ガイドは、移行を「incremental」に進められるものとして書いています。手順は次の3段階です。
- 既存の
ChangeNotifierはそのまま残し、RiverpodのChangeNotifierProviderで公開し直す(3系ではpackage:flutter_riverpod/legacy.dartから読み込む) - ProviderとRiverpodを同じアプリで併用し、Providerを1つずつ移す。ガイドは「it is entirely possible to use both Provider and Riverpod at the same time」と明記し、全部を一度に移さないよう注意しています
- 移し終えたものから
ChangeNotifierをNotifierに書き換える
併用中は両パッケージにChangeNotifierProviderやConsumerという同名のクラスがあるため、ガイドはimportに別名を付けることを勧めています(例:import 'package:provider/provider.dart' as p;)。
RiverpodとBlocの違い
状態を変える入口:メソッド呼び出しとイベント
BlocのREADMEは、Blocを「関数ではなくeventsに依存して状態を変えるクラス」と定義しています。イベントを経由する分だけ型定義は増えますが、onTransitionをオーバーライドすると「変更前の状態・イベント・変更後の状態」の3つが1回の遷移ごとに受け取れます。どの操作で状態が変わったかをログに残したい、操作履歴を監査したいという要件には、この仕組みがそのまま使えます。
Blocのパッケージにはイベントを使わないCubitもあり、こちらはemitを呼ぶメソッドで状態を変えます。書き味はRiverpodのNotifierに近く、「Blocは必ずイベントを書く」というわけではありません。
非同期処理の扱い:AsyncValueと状態クラスの自前定義
Riverpodでは、FutureProviderやAsyncNotifierProviderを購読した値がAsyncValueになり、読み込み中・データ・エラーの3状態を画面側でswitchするだけで描き分けられます。Blocで同じことをするには、読み込み中・成功・失敗の状態クラスを自分で定義し、イベントハンドラの中で順にemitします。
見落としやすいのが、Blocのイベント処理の既定動作です。bloc 9.2.1のEventTransformerのAPIドキュメントには「By default events are processed concurrently.」とあり、検索ボタンを連打すると前のリクエストが終わる前に次のハンドラが走ります。イベントを順番に処理したい場合や、新しいイベントの到着時に前のハンドラをキャンセルして最新の処理へ切り替えたい場合は、bloc_concurrencyパッケージのsequentialやrestartableをtransformerに指定します。
テストの書き方:ProviderContainer.testとblocTest
テストのimportは、プロジェクト名をsm7039とし、前掲のコードをlib/riverpod_counter.dart・lib/bloc_counter.dart、後掲のDI定義をlib/di.dartに置いた前提です。テスト用の依存としてflutter_testとbloc_test(10.0.0で確認)をdev_dependenciesに加えます。
RiverpodはProviderContainer.test()でコンテナを作り、ウィジェットなしで状態を読み書きできます。テスト終了時の破棄も自動です。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sm7039/riverpod_counter.dart';
void main() {
test('incrementで1になる', () {
final container = ProviderContainer.test();
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
});
}
Blocはbloc_testパッケージのblocTestで、「このイベントを送ったら、この順で状態が出る」を宣言的に書きます。状態の列を検証するので、遷移の順番そのものが仕様になっている画面ではBlocのほうが意図を表しやすくなります。
import 'package:bloc_test/bloc_test.dart';
import 'package:sm7039/bloc_counter.dart';
void main() {
blocTest<CounterBloc, int>(
'Incrementedで[1]を出す',
build: CounterBloc.new,
act: (bloc) => bloc.add(Incremented()),
expect: () => [1],
);
}
GetX・Redux・MobXと比べたときの判断材料
GetX(パッケージ名get)は、状態管理に加えて画面遷移・DI・多言語化までを1つにまとめたパッケージです。安定版は2025年11月公開の4.7.3ですが、5.0系は2022年1月20日の5.0.0-beta.1から2026年6月3日の5.0.0-release-candidate-9.3.3まで、67のプレリリース版が出たまま安定版になっていません。GetXのルーティングに乗るとFlutter標準のNavigatorやgo_routerへ移るときに書き換え範囲が広がるため、新規案件で採用する理由は見当たりません。既存のGetXアプリは、状態管理だけを先に別ライブラリへ移すと影響範囲を区切れます。
Redux(flutter_redux)は、ストア・アクション・リデューサーで状態を一方向に流す設計で、Web版Reduxの経験者には読みやすい構成です。ただ前述のとおり最終公開が2022年5月で、新しく選ぶ理由は乏しくなっています。
MobX(mobx / flutter_mobx)は2026年9月11日にも更新があり、保守は続いています。観測対象の値(Observable)を読んだ箇所を自動で追跡し、値が変わると再描画する方式で、Riverpodのように読む側がref.watchで購読先を明示する設計とはここが違います。@observableなどのアノテーションを使う書き方ではmobx_codegen(2.8.0)によるコード生成が必要ですが、ObservableやActionを直接書く方式も公式に用意されており、RiverpodもMobXもコード生成なしで使えます。
依存性注入(DI)としてのRiverpodとget_itの使い分け
RiverpodのProviderは状態だけでなく、リポジトリやAPIクライアントのような依存オブジェクトも公開できます。次の例のHttpTodoRepositoryは、通信処理を省略した骨組みで、直接呼ぶとUnimplementedErrorになります。リポジトリを返すProviderと、それを使って一覧を取るFutureProviderを定義しています。
import 'package:flutter_riverpod/flutter_riverpod.dart';
abstract interface class TodoRepository {
Future<List<String>> fetchTitles();
}
class HttpTodoRepository implements TodoRepository {
@override
Future<List<String>> fetchTitles() async => throw UnimplementedError();
}
final todoRepositoryProvider =
Provider<TodoRepository>((ref) => HttpTodoRepository());
final todoTitlesProvider = FutureProvider<List<String>>((ref) {
return ref.watch(todoRepositoryProvider).fetchTitles();
});
テストで本物のリポジトリを偽物に差し替えるとき、Riverpodはコンテナごとにoverridesを渡します。get_itの例はGetIt.Iでアプリ全体の登録先を共有しているため、テスト後に登録を消しています。get_itでもGetIt.asNewInstance()を使えば、テストごとに独立した登録先を作れます。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get_it/get_it.dart';
import 'package:sm7039/di.dart';
class FakeTodoRepository implements TodoRepository {
@override
Future<List<String>> fetchTitles() async => ['牛乳を買う'];
}
void main() {
test('Riverpod: コンテナ単位で差し替える', () async {
final container = ProviderContainer.test(overrides: [
todoRepositoryProvider.overrideWithValue(FakeTodoRepository()),
]);
expect(await container.read(todoTitlesProvider.future), ['牛乳を買う']);
});
test('get_it: グローバルに登録し、テスト後に消す', () async {
GetIt.I.registerSingleton<TodoRepository>(FakeTodoRepository());
addTearDown(GetIt.I.reset);
expect(await GetIt.I<TodoRepository>().fetchTitles(), ['牛乳を買う']);
});
}
Riverpodの差し替えはコンテナの中だけで完結するため、テスト同士が登録内容を共有しません。GetIt.Iを使う書き方では、resetを忘れると次のテストに偽物が残ります。Riverpodを採用するなら、DIのためだけにget_itを足す必要はありません。逆に、状態管理はBlocやsetStateで足りていてDIだけが欲しい案件では、get_it(9.3.0、2026年9月19日公開)のほうが持ち込む概念が少なく済みます。
MVVM・クリーンアーキテクチャでRiverpodが担う層
Flutter公式の状態管理ページが組み込みの手段として挙げているのは、setState、ValueNotifierとInheritedNotifier、InheritedWidgetとInheritedModelの3系統です。公式のアーキテクチャ推奨事項は、ユーザー操作の処理にCommandを使うことを「Recommend」、ウィジェット更新にChangeNotifierとListenableを使うことを「Conditional」と位置づけており、特定の状態管理パッケージを必須にはしていません。複数のViewModelで共有するRepositoryについては、service locatorかDIコンテナで管理する構成を例に挙げています。Riverpodは、このDIコンテナの役割とViewModelの状態保持を1つで担えるのが特徴です。
RiverpodをMVVMで使う場合、ViewModelをNotifierとして書き、Viewはref.watchで購読します。クリーンアーキテクチャのように層を分ける場合も、RepositoryやUseCaseをProviderとして公開し、上位の層がref.watchで下位を参照する形にすれば、依存の向きはProviderの参照関係にそのまま表れます。具体的なViewModelの書き方はFlutterのMVVM実装をRiverpodで書く手順、層の分け方そのものはFlutterにおけるアーキテクチャ設計の基本と重要性で解説しています。
Riverpodを採用しないほうがよい条件
Riverpodが合わない案件は、次の4つです。
- Flutter 3.44.0未満に固定されている:flutter_riverpod 3.4.1以降の
environment.sdkは^3.12.0です。pubspec.yamlに^3.4.3と書くと古いFlutterでは依存解決が失敗し、^3.0.0のような幅のある指定では、エラーにならずSDK要件を満たす旧版が選ばれることがあります。3.0.1〜3.3.2のSDK要件は^3.7.0なので、Dart 3.7〜3.11の環境なら3.3系に落ち着きます。実際に採用される版は他パッケージの制約とロックファイルでも変わるため、flutter pub depsで確かめてください。 - Blocの遷移ログを監査や障害調査に使っている:
onTransitionでイベント単位の記録を取っているチームがRiverpodへ移ると、同じ粒度の記録を自前で作り直すことになります。 - 状態が1画面の中で閉じている:フォームの入力途中やタブの選択位置のように他画面と共有しない値は、
StatefulWidgetのsetStateで足ります。判断基準はStatelessWidgetとStatefulWidgetの違いと使い分けにまとめています。 - Providerで動いていて困りごとがない:Providerは保守が続いており、移行そのものが目的になると工数だけがかかります。
ProviderNotFoundExceptionが頻発する、非同期の状態管理が画面ごとにばらばら、といった具体的な問題が出てから移るので十分です。
「コード生成(build_runner)をCIに入れたくない」は不採用の理由になりません。Riverpodは@riverpodのコード生成なしでも、この記事のコードのように手書きで使えます。
よくある質問
RiverpodとProviderはどちらを使うべきですか?
新規開発で非同期データの取得・共有が多いならRiverpodを第一候補にしてください。Providerで書かれた既存アプリは、困りごとがなければそのままで問題ありません。両者は同じ作者のパッケージで、Providerも2025年8月に6.1.5+1が公開されるなど保守は続いています。
RiverpodとBlocはどちらが大規模開発に向いていますか?
規模ではなく、状態遷移の記録が必要かどうかで決めます。イベント単位で「何が起きて状態がどう変わったか」を追う必要があるならBloc、非同期データの取得と表示が中心ならRiverpodが向いています。BlocもCubitを使えばイベント定義なしで書けます。
RiverpodはGoogleの公式パッケージですか?
いいえ。発行者はdash-overflow.net(作者のRemi Rousselet氏)で、Googleではありません。riverpodとproviderはpub.devで「Flutter Favorite」に選ばれています。Flutter公式はこの制度を「アプリ開発でまず検討すべきパッケージ」を示すものとし、品質の保証ではないとも説明しています。Googleが開発に関わっていることを示す印ではありません。
StateProviderはRiverpod 3でも使えますか?
使えますが、通常のimportからは外れ、package:flutter_riverpod/legacy.dartから読み込む扱いになりました。新しく書くならNotifierとNotifierProviderを使ってください。書き換え方はRiverpod NotifierProviderの使い方とStateNotifierProviderとの違いで解説しています。
Riverpodを使うならget_itは不要ですか?
不要です。リポジトリやAPIクライアントもProviderとして公開でき、テストではoverridesで差し替えられます。get_itが活きるのは、状態管理にRiverpodを使わずDIだけが欲しい場合です。