Flutter Riverpodとは?初心者でもわかるシンプルな状態管理

モバイル開発

皆さんはFlutterでアプリを開発するときに、画面やコンポーネント同士のデータの受け渡しについて悩んだことはないでしょうか。 多くの要素が連動するアプリでは、データの整合性を保ちながら機能を実装することが重要になります。

そんなときに便利なのが Riverpod です。 これはFlutterの状態管理をより扱いやすくするためのパッケージで、Providerを拡張したようなイメージを持っていただくとわかりやすいかもしれません。

また、Riverpodはリビルド範囲を細かく制御しやすく、デバッグ機能も充実しています。 この記事では、初心者でもわかりやすいようにRiverpodを使った基本的な状態管理の方法と、その実務での活用シーンを解説します。

本記事はRiverpod 2系に基づきます。

Flutterにおける状態管理の重要性

FlutterはWidget中心でUIを構築しますが、Widgetが多くなるほど状態管理が複雑になります。 一覧画面から詳細画面へのデータ受け渡しや、ネットワーク通信の結果によって画面を更新するなど、さまざまなシチュエーションで状態管理を検討する必要があります。

小規模なアプリであれば、setStateを使って画面を再描画すれば十分な場合もあります。 しかし、機能が増えたり画面数が増えたりすると、単純なsetStateではコードが複雑になりやすいのです。 そのため、別の構造化されたアプローチが必要になります。

Riverpodは、この複雑化を抑えるための選択肢として人気があります。 ProviderやBLoCなど、他の状態管理手法と比較してもシンプルに書ける面があり、必要に応じて段階的に機能を拡張できます。

また、実際の案件ではUIロジックや認証情報、ユーザーデータの共有など、Riverpodを活用できる場面が多くあります。 画面間でデータをやり取りするときにも、冗長なコードを書かずに済む場合があるため、開発コストの削減にもつながります。

Riverpodの特徴とメリット

RiverpodはProviderパッケージの設計を参考にしながら、独自の改良を施した状態管理フレームワークです。 まず、Riverpodを使うとアプリ全体のどこからでも同じプロバイダを参照しやすくなります。

また、ProviderScopeがウィジェットツリーを包み込む構造になっており、このProviderScopeによって異なる画面間でもデータを簡単に共有できます。 そして、Riverpodではプロバイダごとにスコープを切り替えることもできるため、データの使い分けが柔軟に行えます。

エラーやデバッグ情報を確認しやすい点もメリットです。 Flutterの開発ではホットリロードを繰り返すことが多いですが、Riverpodはその過程で状態を再設定しやすいため、開発効率を高められます。

さらに、AsyncValueのような仕組みを使うと、非同期処理を行うときに、データのロード中・完了・エラーそれぞれに応じた状態を1つのクラスで扱えます。 これにより、読み込み中のローディング表示やエラーメッセージの表示を一貫して行いやすくなります。

実務レベルで考えても、認証画面でログイン状態を管理したり、APIコールのレスポンスをAsyncValueで扱ったりするケースは珍しくありません。 コードが整理されて読みやすくなり、複数人の開発体制でも保守が楽になることが多いです。

Riverpodの基本的な使い方

Riverpodを使うときは、まずpubspec.yamlに以下のように依存関係を追加します。 ここでは最新版を想定しています。

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.3.0

その後、MaterialAppを包むルートウィジェットを変更して、ProviderScopeを最上位に配置します。

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

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

ProviderScopeがルートにあることで、アプリ全体でRiverpodのプロバイダを利用できます。 あとは、状態を扱いたい部分でプロバイダを宣言し、ConsumerWidgetなどから参照すればOKです。 これによって、どの画面からでも同じプロバイダを共有しやすくなります。

プロバイダの宣言と読み取り

Riverpodでは、状態を表す「プロバイダ」を作成して管理します。 最も基本的なのが StateProvider で、これは単純な変数のように値を保持できます。

final counterProvider = StateProvider<int>((ref) => 0);

class HomeScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text('Riverpod Counter'),
      ),
      body: Center(
        child: Text('カウント: $counter'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).state++;
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

この例では、 counterProvider というStateProviderを宣言し、初期値を0にしています。 ConsumerWidgetref.watch(counterProvider) を呼び出すと、その値が変更されるたびにUIが再ビルドされます。 実務でもカウンター以外に、例えばユーザー名や商品一覧の件数などを管理するときに似た書き方をします。

非同期処理とAsyncValue

実際の開発では、APIコールなどの非同期処理を扱う場面が多いです。 Riverpodでは、これを管理するためにFutureProviderやStreamProviderが用意されています。 さらに、AsyncValue型によって、読み込み中・成功・エラーの状態を一元的に扱えます。

例えば、ユーザー情報をAPIから取得する場合は、次のように書きます。

final userProvider = FutureProvider<User>((ref) async {
  // 仮のAPI呼び出し
  await Future.delayed(Duration(seconds: 2));
  return User(name: 'Taro', age: 30);
});

class UserScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsyncValue = ref.watch(userProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('ユーザー情報'),
      ),
      body: userAsyncValue.when(
        loading: () => Center(child: CircularProgressIndicator()),
        error: (err, stack) => Center(child: Text('エラーが発生しました')),
        data: (user) => Center(
          child: Text('名前: ${user.name}, 年齢: ${user.age}'),
        ),
      ),
    );
  }
}

FutureProvider を使うと、結果の状態をAsyncValueとして受け取れます。 UI側は when メソッドを使い、読み込み中・エラー・成功時の表示を分けることができます。 実務でもAPI通信のローディング表示やリトライ機能などを簡単に実装できるので、多くのアプリで重宝されています。

実務での活用シーンと注意点

Riverpodは、画面をまたいだデータの共有や非同期処理が多いアプリに特に向いています。 たとえば、SNSアプリのタイムラインやECサイトのカート情報など、複数の画面で同じデータを参照するケースで力を発揮します。

一方で、使い方を誤るとプロバイダが過度に乱立し、状態がどこにあるのか分かりにくくなることもあります。 そのため、どのデータをどのスコープで管理するのかを、あらかじめ決めておくと良いでしょう。

また、テストを書くときには各プロバイダごとにモックを差し替えるなどの工夫が必要になります。 ただ、Riverpod自体がテストしやすい構造を提供しているため、適切に設計すれば大きな問題は起こりにくいです。

複数人での開発においては、プロジェクト全体でプロバイダの命名規則や配置場所をあらかじめ相談すると、可読性が保たれます。 このようなルールをチームで共有しておくと、後から新しいメンバーが参加してもコードを理解しやすくなります。

Riverpodを導入する際は、どこでデータを管理するかを明確にしておくと管理しやすくなります。

よくある疑問とQ&A

Q: Providerとの違いは何でしょうか。

RiverpodはProviderの改良版ともいわれますが、大きな違いとしては、プロバイダ同士の依存関係を明示的に扱える点や、リビルドの範囲をより細かく制御できる点が挙げられます。 また、ProviderScopeによってアプリ全体でのスコープ管理が楽になり、テストのしやすさも向上しています。

Q: ReduxやBLoCと比べてどうですか。

ReduxやBLoCも人気がありますが、どれがベストというより、プロジェクトの規模やチームの好みに応じて選ぶことが多いです。 Riverpodは比較的シンプルに導入しやすく、学びやすいという声が多いです。 ただ、大規模なアプリではReduxのような一元管理の方法が合う場合もあります。

Q: StateNotifierやChangeNotifierを使うタイミングがわかりません。

Riverpodでは、StateProvider以外にも、StateNotifierProviderなどがあり、ビジネスロジックをより明確に分離したいときに重宝します。 UIの状態とビジネスロジックをはっきり分けたいなら、StateNotifierを使って状態管理を明確化するのが良いでしょう。

まとめ

Flutterアプリ開発では、データの管理方法をどうするかがプロジェクトの質を左右する大きな要素になりがちです。 Riverpodは、その点をシンプルに解決してくれる便利な手段として注目を集めています。

画面同士の連携や、複数の非同期処理が絡むような実務の場面でも、Riverpodを使うことでコードの整理がしやすくなるでしょう。 必要に応じて、StateProvider、FutureProvider、StreamProvider、そしてStateNotifierProviderなどを組み合わせれば、ほとんどの状態管理シナリオに対応できます。

どのような開発規模でも、データの整合性を保ちながらわかりやすいコードを書きたいと考える皆さんには、Riverpodは魅力的な選択肢になるのではないでしょうか。 まずは小さなプロジェクトから導入し、操作感や設計方針を学んでみると、より本格的なアプリ開発にスムーズに活かせます。 ぜひ皆さんもFlutterでの状態管理にRiverpodを検討してみてください。

Flutterをマスターしよう

この記事で学んだFlutterの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。