6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Riverpod (Flutter Roadmap Riverpod)

Posted at

はじめに

Flutterを網羅的に学習するにあたってRoadmapを使って学習を進めることにしました。

この記事では、Flutter初学者やこれからFlutterを学習し始める方に向けて、Riverpodについてまとめています。

RoadmapはFlutterだけでなく、他の言語やスキルのロードマップも提供されており、何から学習して良いか分からないと悩んでいる方にとって有用なサイトになっています。
ぜひRoadmapを利用して学習してみてください。

Roadmapとは

簡潔に言えば、Roadmap.shは学習者にとってのガイドブックであり、学習の方向性を提供する学習ロードマップサイトです。

初心者から上級者まで、ステップバイステップでスキルを習得するための情報が提供されています。

学習の進め方が分かりやすく示されているだけでなく、個々の項目に参考資料やリソースへのリンクも提供されているので、学習者は目標を設定し、自分自身のペースで学習を進めることができます。

Riverpod

FlutterロードマップRiverpodでは以下の5つのサイトが紹介されています。興味のある方はぜひお読みください。

Riverpodとは

Riverpodは、Flutterアプリケーションのための状態管理ライブラリで、Provider パッケージのようにウィジェットツリー内でデータを提供するために使用されます。Riverpodは、Provider パッケージを改良し、柔軟性と拡張性を向上させたもので、複雑な状態を簡単に管理することができるライブラリです。

Riverpodを使用することで、任意の Widget にデータを受け渡すことが出来るようになります。

Riverpodの特徴

  • スコープの明確化: Riverpodでは、プロバイダーのスコープがより厳密に定義されてい流ため、Providerごとに異なるスコープを持たせることができ、依存関係をより制御しやすくしています。

  • リアクティブなデータ変更: リアクティブな変更通知を提供しており、データが変更された際に関連するウィジェットが自動的に再構築される仕組みをサポートしています。

  • 様々なProvider: Riverpodでは、 Provider だけでなく、 FutureProviderStreamProvider など、異なるデータソースに対するプロバイダーが統一的なAPIで提供されています。

  • 依存関係の設定: 依存関係を自動的に解決し、プロバイダー間の依存関係を簡単に設定できるようになっています。

  • Null Safety: Null Safety をサポートしており、安全性と安定性が向上しています。

  • 利用範囲の広さ: 小規模なアプリケーションから大規模なアプリケーションまで幅広く利用できます。プロバイダーのスコープの明確さと柔軟性により、アプリケーションのサイズに合わせて効果的に使用できます。

  • リアクティブなUIの構築: リアクティブな変更通知をサポートしているため、データの変更に応じてUIが自動的に更新されるようなアプリケーションに適しています。

  • 非同期データの処理: FutureProviderStreamProvider を使用して非同期データを処理し、リアルタイムなデータの表示や取得が必要な場合に有用です。

  • テスト容易性: グローバル変数として定義するため、アクセスが保証されるのと同時にテスト容易性も担保されます。複雑な setUp / tearDown ステップが手順が不要になり、Providerを上書きできるので特定の動作テストが簡単です。

  • パフォーマンスの向上: 「状態」を別の「状態」と組み合わせることも簡単に行え、状態の変化によって影響を受けるもののみが再計算されるのでWidgetの再構築が最適化されます。

Providerとの違い

Providerパッケージは InheritedWidget を改良する形で開発されたパッケージでWidgetツリーに依存しています。親のWidgetツリーを購読して、登録されている Provider にアクセスするため、親のWidgetツリーには使用したい Provider が登録されている必要があります。

もし Provider が登録がされていない場合は ProviderNotFoundException エラーが発生してしまいます。

一方でRiverpodパッケージは、 Provider をWidgetツリーから切り離してグローバルに定義する事が出来るため、定義した Provider に確実にアクセスする事が出来ます

また、Providerパッケージでは同じ型のものが複数同時に使用できないのに対して、Riverpodパッケージでは同じ型の Provider を複数参照できるため、Providerパッケージの欠点を補ってくれています。

Riverpodパッケージ

Riverpodのパッケージには3種類あります。

パッケージ名 形態 説明
flutter_riverpod Flutterのみ RiverpodをFlutterと統合するためのパッケージです。Flutterアプリケーション内での Riverpod の利用においては、通常このパッケージを使用します。
hooks_riverpod Flutter + flutter_hooks ReactのHooks APIのような、ウィジェットを構築するためのフック(Hooks)を提供します。再利用可能で理解しやすいコードを書くことができるため、Riverpod をよりシンプルに使用するための手段として使用されます。
riverpod Dart のみ Flutterに関連するすべてのクラスが削除された、Dartのみのバージョンです。Flutter機能を使用しないパッケージではこちらを使用することになります。

使い方

1. ProviderScopeをルートに追加

Riverpodを使った Provider の宣言自体はグローバルですが、 Provider をアプリ内で利用するために範囲(スコープ)を指定する必要があります。アプリのルート部分を ProviderScope でラップすることで、下位ツリーのWidgetで Provider を呼び出すことができるようになります。。

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

2. Providerをグローバル変数として定義

プロバイダの作成方法には手動でコードを設定する方法とコード生成を使用する方法の2種類があります。

手動で設定

Provider の引数に指定したコールバック関数で return するものが管理するデータになります。コールバック関数の引数の ref は他のProviderのデータを取得したい時に使用します。Providerのデータを取得していない場合は、 ref ではなく _ として問題ありません。以下は手動設定の場合のサンプルコードです。

手動で設定
final myProvider = Provider<MyApp>((ref) {
  return MyApp();
});
// アロー構文を使用した場合
final myProvider = Provider((ref) => MyApp());
  • final myProvider は、定数の宣言です。定数名には使用する Provider の名前を定義します。

  • Provider<T> で Riverpod で使う Provider の種類を設定しています。

  • <MyApp> は型の宣言を行っています。この ProviderMyApp という型を返すということを明示的に示しています。

  • ((ref) { return MyApp(); }); では、 ref という引数を受け取り、 MyApp() という値を返しています。ref を使用しない場合は ((_) { return MyApp(); }); のようにして省略することも可能です。

  • refRiverpod のRefオブジェクトを表します。Refを使うことで他のプロバイダーにアクセスしたり、状態の変更を通知したりできます。以下は ref の使用例です。

  • 他のプロバイダーを読み込む

final user = ref.read(userProvider);
  • StateNotifierの変更を通知する
ref.read(countProvider.notifier).update((state) => state + 1);
  • Riverpodで提供されているメソッドを利用する
ref.listen<String>(helloProvider, (prev, next) {
  // 状態変更時の処理 
});

コード生成

コード生成
part 'my_provider.g.dart';

@riverpod
MyApp my(MyRef ref) {
  return MyApp();
}
  • コード生成を使用するので、 part: '{file_name}.g.dart'; の宣言が必須です。

  • @riverpod アノテーションをつけて、プロバイダ生成のためのメソッドであることを示します。

  • メソッド先頭のMyAppは、関数の戻り値の型を指定しています。

  • 関数名である my はプロバイダの名前になります。設定で変更することもできますが、コード生成を行うと末尾に Provider が自動で付くので、 myProvider という名前になります。

  • 関数のパラメータである (MyRef ref) はコード生成される MyRef を関数内で使用するためのものです。コード生成前はコンパイルエラーになりますが、コード生成を実行すれば生成されるものなので問題ありません。

3. Providerからデータを取得する

Provider からデータを取得するためには ConsumerWidget を使用します。 ConsumerWidget を継承した Widget を定義すると、 build()WidgetRef 型の引数が追加されます。 追加された WidgetRef 型の引数に watch() または read() のメソッドを使用して、 Provider を指定することで Provider が管理しているデータを取得することができます。

データの変更を監視する必要がある時は watch() を、監視する必要がない時は read() を使用してデータを取得します。以下のコードはProviderからデータを取得するカウンタアプリのサンプルコードです。

カウンタアプリサンプルコード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CountNotifier extends StateNotifier<int> {
  CountNotifier() : super(0);

  void increment() => state++;

  void decrement() => state--;
}

final countProvider =
    StateNotifierProvider<CountNotifier, int>((ref) => CountNotifier());

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Riverpod Counter',
      home: MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(countProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod Counter'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Count',
              style: TextStyle(fontSize: 32),
            ),
            Text(
              '$count',
              style: const TextStyle(fontSize: 40),
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              ref.read(countProvider.notifier).increment();
            },
            child: const Icon(Icons.add),
          ),
          const SizedBox(width: 8),
          FloatingActionButton(
            onPressed: () {
              ref.read(countProvider.notifier).decrement();
            },
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
  • CountNotifier:

    • CountNotifierStateNotifier を継承し、整数の状態を管理するクラスです。
    • コンストラクタで初期値 0 を指定しています。
    • increment は状態を増やし、 decrement は状態を減らすメソッドです。
  • countProvider:

    • StateNotifierProvider を使用して、 CountNotifier のインスタンスを提供するプロバイダーを作成しています。このプロバイダーは CountNotifier の状態を取得するために使用されます。
  • MyHomePage:

    • ConsumerWidget を継承し、プロバイダーの状態を監視するためのウィジェットです。 ref.watch(countProvider)countProvider の状態を取得しています。また、 ref.read(countProvider.notifier)CountNotifier のメソッドにアクセスしています。
    • ボタンが押されると、 incrementdecrement それぞれのメソッドが呼び出され、状態が変更されます。
    • final count = ref.watch(countProvider); で状態を監視しているため、ボタンが押されて状態が変更されるたびに、Textが更新されていきます。

主なProvider

名前 概要
Provider 簡単な状態を提供する最も基本的なプロバイダであり、値を同期的に生成する
StateProvider 外部から変更が可能な状態を公開するプロバイダ、ステートの管理にわざわざ StateNotifier クラスを定義するほどではない場合に使用する。
StateNotifierProvider StateNotifierを監視し、公開するためのプロバイダ
ChangeNotifierProvider ChangeNotifier を Flutter で利用するためのプロバイダ、Riverpod では使用を非推奨
FutureProvider Futureをラップした非同期操作可能なProvider 非同期で取得した値を提供する
StreamProvider Streamをラップした非同期操作可能なProvider 断続的に最新の値を提供する

Provider

外部から変更することができないデータを管理する基本的な Provider です。状態の変更通知などは自動的に行われず、単なるデータの提供に焦点が当てられています。

計算結果をキャッシュしたり、変更することのないデータを渡す際などに使用されます。キャッシュの例でいくと、フィルタリング機能が挙げられます、データにフィルタを適用する場合、画面が再描画される度にフィルタを適用すると余計な処理が入ってしまいます。そこで、 Provider を利用することで、フィルタリングしたデータをキャッシュさせることができ、都度処理が呼ばれるのを防ぐことができるのです。

サンプルコード

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


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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Riverpod Counter',
      home: MyHomePage(),
    );
  }
}

final strProvider = Provider((ref) {
  return 'Hello Riverpod';
});

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(strProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod Sample'),
      ),
      body: Center(
        child: Text(
          value,
          style: const TextStyle(
              fontSize: 24
          ),
        ),
      ),
    );
  }
}
  • strProvider は、 RiverpodProvider を使用して定義されています。このプロバイダーは文字列 'Hello Riverpod' を返しています。

  • MyHomePage クラスは、 ConsumerWidget を継承し、 build メソッドでUIを構築しています。 strProvider の値を ref.watch で取得し、それをテキスト表示しています。

StateProvider

外部から変更することができるデータを管理するProviderです。StateNotifierProvider の簡易版で、ステートの管理に StateNotifier クラスを定義するほどではないシンプルなステートを管理する場合に使用します。

外部からデータを変更する時は StateController インスタンスの update() 、または直接 state に変更を加えることでデータを変更します。

シンプルなステートを管理する場合

  • 列挙型(enum)、フィルタの種類など
  • 文字列型、テキストフィールドの入力内容など
  • bool 型、チェックボックスの値など
  • 数値型、ページ数やフォームの年齢など

StateProviderを使用すべきではないステート

  • ステートの算出に何かしらのバリデーション(検証)ロジックが必要な場合
  • ステート自体が複雑なオブジェクトである場合(カスタムのクラスや List/Map など)

上記のような複雑なステートを管理する場合は、 StateNotifierProvider を使用することが推奨されています。

サンプルコード

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

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

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

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

class Product {
  Product({required this.name, required this.price});

  final String name;
  final double price;
}

final _products = [
  Product(name: 'iPhone', price: 999),
  Product(name: 'cookie', price: 2),
  Product(name: 'ps5', price: 500),
];

enum ProductSortType {
  name,
  price,
}

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);

final productsProvider = Provider<List<Product>>((ref) {
  final sortType = ref.watch(productSortTypeProvider);
  switch (sortType) {
    case ProductSortType.name:
      return _products.sorted((a, b) => a.name.compareTo(b.name));
    case ProductSortType.price:
      return _products.sorted((a, b) => a.price.compareTo(b.price));
  }
});

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final products = ref.watch(productsProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Products'),
        actions: [
          DropdownButton<ProductSortType>(
            value: ref.watch(productSortTypeProvider),
            onChanged: (value) =>
                ref.read(productSortTypeProvider.notifier).state = value!,
            items: const [
              DropdownMenuItem(
                value: ProductSortType.name,
                child: Icon(
                  Icons.sort_by_alpha,
                  color: Colors.black,
                ),
              ),
              DropdownMenuItem(
                value: ProductSortType.price,
                child: Icon(
                  Icons.sort,
                  color: Colors.black,
                ),
              ),
            ],
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return ListTile(
            title: Text(product.name),
            subtitle: Text('${product.price} \$'),
          );
        },
      ),
    );
  }
}
  • Product クラス:

    • Product クラスは商品を表すデータモデルです。商品には名前(name)と価格(price)が含まれています。
  • _products リスト:

    • _products リストには、 Product クラスのインスタンスが複数含まれています。これはサンプルの商品データです。
  • ProductSortType:

    • ProductSortType は、商品のソート方法を示すために使用されます。 name の場合は名前でソートし、 price の場合は価格でソートします。
  • productSortTypeProviderプロバイダー:

    • productSortTypeProvider は、商品をソートする方法(名前か価格か)を監視します。
  • productsProvider プロバイダー:

    • productsProvider は、現在のソート方法に基づいて商品リストを返しています。
  • MyHomePage ウィジェット:

    • MyHomePage クラスは ConsumerWidget を継承し、 build メソッドでUIを構築しています。商品リストが表示され、AppBarには商品をソートするドロップダウンメニューがあります。
    • 商品リストは ListView.builder を使用して構築され、各商品は ListTile で表示されます。
  • ドロップダウンメニュー onChanged

onChanged: (value) =>
    ref.read(productSortTypeProvider.notifier).state = value!,
  • read メソッドで、 プロバイダーの値を読み取り、 productSortTypeProvider.notifierで、 StateProvider の更新メソッドにアクセスし、 state = value! で状態を更新しています。

上記のように state を使用して新たなステートを作成することもできますが、 現在の状態をパラメータとして利用できること、状態のイミュータブルな更新が保証されることから update メソッドを使うことが推奨されています。

上記のコードを update メソッドを使用して書くと以下のようになります。

onChanged: (value) =>
    ref.read(productSortTypeProvider.notifier).update(
          (state) => state = value!,
        ),

StateNotifierProvider

StateNotifier を監視し、データとして管理するためのプロバイダです。変化するステートを管理するソリューションとして推奨されています。

一般的には以下のような場面で使用されます。

  • イミュータブル(不変)なステートを管理する場合 (イミュータブルではあるが、イベントに応じて状態が変わることがある場合)。
  • ステートを変更するためのロジックを一つの場所で集中管理する場合。

以下のサンプルコードは、 StateNotifierProvider を使用して Todo リストを作成しています。 StateNotifierProvider を使うことで addTodo などのメソッドをUI 側に渡して Todo リストの内容を操作できるようになります。

サンプルコード

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

part 'main.freezed.dart';

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

@freezed
class Todo with _$Todo {
  factory Todo({
    required String text,
  }) = _Todo;
}

class TodoListNotifier extends StateNotifier<List<Todo>> {
  TodoListNotifier() : super([]);

  void addTodo(String text) {
    state = [...state, Todo(text: text)];
  }

  void removeTodo(int index) {
    state = List.from(state)..removeAt(index);
  }
}

final todoListProvider = StateNotifierProvider<TodoListNotifier, List<Todo>>(
      (ref) => TodoListNotifier(),
);

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Todo List',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  MyHomePage({Key? key}) : super(key: key);

  final TextEditingController _textController = TextEditingController();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(todoListProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo List'),
      ),
      body: ListView.builder(
        itemCount: state.length,
        itemBuilder: (context, index) {
          final todo = state[index];
          return ListTile(
            title: Text(todo.text),
            trailing: IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () {
                ref.read(todoListProvider.notifier).removeTodo(index);
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddTodoDialog(context, ref),
        child: const Icon(Icons.add),
      ),
    );
  }

  Future<void> _showAddTodoDialog(BuildContext context, WidgetRef ref) async {
    return showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Add Todo'),
          content: TextField(
            controller: _textController,
            decoration: const InputDecoration(
              hintText: 'Enter your todo',
            ),
          ),
          actions: <Widget>[
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
                _textController.clear();
              },
              child: const Text('Cancel'),
            ),
            TextButton(
              onPressed: () {
                final newText = _textController.text.trim();
                if (newText.isNotEmpty) {
                  ref.read(todoListProvider.notifier).addTodo(newText);
                }
                Navigator.of(context).pop();
                _textController.clear();
              },
              child: const Text('Add'),
            ),
          ],
        );
      },
    );
  }
}
  • Todoクラス:

    • Todo クラスはFreezedパッケージによって生成された不変のデータクラスです。 @freezed アノテーションを使用して Freezed クラスとして宣言されています。
    • _$Todo は、Freezedが生成するファクトリーコンストラクタの名前です。 _$Todo の$はアンダースコアに続く文字を大文字に変換するFreezedの慣習です。
    • factory キーワードを使ってコンストラクタを定義しており、 requiredtext パラメータを受け取る Todo() コンストラクタが作成されています。
    • _Todo クラスは Freezed によって生成される、不変のデータクラスです。このクラスのインスタンスは一度作成されたら変更できません。これにより、不変性が確保され、安全性が向上します。
  • TodoListNotifierクラス:

    • TodoListNotifier クラスは StateNotifier を継承しており、 List<Todo> の状態を管理しています。
    • state プロパティは StateNotifier クラスが管理する現在の状態を表しており、 state = で、新しい状態を指定することができます。
    • addTodo メソッドの [...state, Todo(text: text)] は、スプレッド演算子 (...) を使用して、現在の状態 (state) の要素を新しいリストに展開し、その後 Todo オブジェクトを追加しています。
    • removeTodo メソッドの List.from(state) は、現在の状態を新しいリストに複製しています。この複製を作成することで、元のリストに変更が加えられても元のリスト自体は変更されないため、不変性が保たれます。 また、 ..removeAt(index) で、複製されたリストから指定された index の位置にある要素を削除しています。このカスケード演算子 (..) は、同じオブジェクトに対して複数の操作を行うために使用されます。
  • todoListProvider:

    • StateNotifierProvider では、 TodoListNotifier を使用して List の状態を提供しているため、アプリケーション内の異なる部分で同じTodoリストを共有することができます。
    • StateNotifierProvider のジェネリクス型引数は、2つの型引数を取ります。第一引数 (TodoListNotifier) は、状態を管理する StateNotifier の型です。第二引数 (List) は、StateNotifier が管理する状態の初期値の型です。
    • (ref) => TodoListNotifier() は、Providerが初めて読み込まれたときに呼び出される初期化関数です。
  • MyHomePageウィジェット:

    • MyHomePage クラスは ConsumerWidget を継承し、ウィジェットツリー内で todoListProvider を監視しています。
    • ref.watchref.read を使用して、Todoリストの表示やTodoの追加・削除の機能を実装しています。

ChangeNotifierProvider

ChangeNotifierProvider は ChangeNotifier を Flutter で利用するためのプロバイダです。Riverpod では以下の理由により、使用を非推奨にしています。可能な限り StateNotifierProvider を使用し、 ChangeNotifierProvider の使用はミュータブルなステート管理を行う必然性がある場合に限定してください。

  • package:providerChangeNotifierProvider を利用していた際の移行作業を容易にするため
  • ミュータブル(可変)なステート管理をサポートするため

データの更新を監視元に伝えたい時は、 notifyListeners() を実行する必要があります。notifyListeners() を記載していないと、データを更新しても監視元に伝わらないので注意してください。

サンプルコード

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

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Todo List',
      home: MyHomePage(),
    );
  }
}

class Counter extends ChangeNotifier {
  int counter = 0;

  void increment() {
    counter++;
    notifyListeners();
  }
}

final counterProvider = ChangeNotifierProvider((ref) {
  return Counter();
});

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod Sample'),
      ),
      body: Center(
        child: Text(
          state.counter.toString(),
          style: const TextStyle(fontSize: 24),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: ref.read(counterProvider).increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • Counterクラス:

    • ChangeNotifier を継承した Counter クラスがあります。 Counter クラスは、カウンターの値を保持し、 increment メソッドを使用して値を増やし、変更があった場合にリスナーに通知します。
  • ChangeNotifierProvider:

    • ChangeNotifierProvider は、 Counter クラスのプロバイダーを作成しています。これにより、 Counter クラスの状態が変更されたときにウィジェットツリーがリビルドされるようになります。
  • MyHomePageウィジェット:

    • MyHomePageConsumerWidget を継承しており、 ref.watch(counterProvider) を使用して Counter の状態を監視し、 ref.read(counterProvider) を使用して Counter のメソッドを呼び出しています。
    • カウンターの値を表示するTextウィジェットがあります。この値は state.counter から取得され、 Counter の状態に変更があると自動的に更新されます。
    • FloatingActionButton をタップすると、 controller.increment が呼び出されてカウンターが増加し、UIが更新されます。

FutureProvider

FutureProvider は非同期操作が可能な Provider で、 Future から取得したデータを管理する際に使用します。

主に以下のような用途で使われます。

  • 非同期操作を実行し、その結果をキャッシュするため。(ネットワークリクエストなど)
  • 非同期操作の error状態、loading状態を適切に処理するため。
  • 非同期的に取得した複数の値を組み合わせて一つの値にするため。

FutureProvider は ref.watch を使用することで、何かの値が変わったときに自動でデータを再取得するよう設定でき、プロバイダが常に最新データを渡すことを保証できます。

サンプルコード

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

Future<String> fetchData() async {
  await Future.delayed(const Duration(seconds: 2)); 
  return 'Hello, Riverpod!';
}

final futureProvider = FutureProvider<String>((ref) => fetchData());

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'FutureProvider Sample',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue<String> data = ref.watch(futureProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('FutureProvider Sample'),
      ),
      body: Center(
        child: data.when(
          loading: () => const CircularProgressIndicator(),
          error: (error, stack) => Text('Error: $error'),
          data: (value) => Text(
            value,
            style: const TextStyle(fontSize: 24),
          ),
        ),
      ),
    );
  }
}
  • fetchData関数:

    • fetchData メソッドは非同期で2秒待機した後、'Hello, Riverpod!'という文字列を返す関数です。
  • FutureProvider:

    • FutureProvider は非同期データを提供するためのProviderです。この場合、 Future<String> 型の非同期関数である fetchData を使用して非同期データを提供しています。
  • MyHomePageクラス:

    • MyHomePageConsumerWidget を継承しています。
    • ref.watch(futureProvider) を使用して非同期データを取得し、 AsyncValue のwhenメソッドを使用して非同期データの状態に応じてUIを構築しています。ローディング中は CircularProgressIndicator が表示され、エラーが発生した場合はエラーメッセージが表示され、データが利用可能な場合はそのデータが表示されます。
    • when() のdata 引数には非同期処理が完了した時に表示するウィジェットを返すコールバック関数、loading 引数には非同期処理を実行している時に表示するウィジェットを返すコールバック関数、error 引数には非同期処理に失敗した時に表示するウィジェットを返すコールバック関数をそれぞれ指定します。
    • error: エラーの詳細情報が含まれるオブジェクトです。通常、これはException型のオブジェクトですが、FutureProviderが提供するエラーには他にも様々な型があります。エラーが何であるかは非同期データを提供しているプロバイダーに依存します。例えば、通信エラーの場合はDioErrorなど。
    • stack: エラーが発生したときのスタックトレースが含まれるオブジェクトです。通常、デバッグやエラーのトラブルシューティングに使用されます。

StreamProvider

StreamProvider は FutureProvider の Stream 版です。

一般的には以下のような用途で使われます。

  • Firebase や WebSocket の監視するため。
  • 一定時間ごとに別のプロバイダを更新するため。

Stream 自体が値の更新を監視する性質を持つため、 StreamBuilder で十分ではないかと思うかもしれません。しかし、 StreamBuilder の代わりに StreamProvider を使用することで以下の利点を得ることができます。

  • 他のプロバイダで ref.watch を通じてストリームを監視することができる。
  • AsyncValue により loading/error のステートを適切に処理することができる。
  • ストリームから出力された直近の値をキャッシュしてくれる。(途中で監視を開始しても最新の値を取得することができる)
  • StreamProvider をオーバーライドすることでテスト時のストリームを簡単にモックすることができる。

サンプルコード

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

Stream<String> fetchData() {
  return Stream.periodic(const Duration(seconds: 1), (value) {
    return 'Data $value from stream';
  });
}

final streamProvider = StreamProvider<String>((ref) => fetchData());

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'StreamProvider Sample',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue<String> data = ref.watch(streamProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('StreamProvider Sample'),
      ),
      body: Center(
        child: data.when(
          loading: () => const CircularProgressIndicator(),
          error: (error, stack) => Text('Error: $error'),
          data: (value) => Text(
            value,
            style: const TextStyle(fontSize: 24),
          ),
        ),
      ),
    );
  }
}
  • fetchData関数:

    • fetchData 関数は、1秒ごとに新しいデータを生成する非同期なストリームを作成しています。
    • Stream.periodic は、一定の時間間隔で定期的に値を生成するストリームを作成するメソッドです。
    • const Duration(seconds: 1) で1秒ごとに新しい値を生成するように指定しています。
  • StreamProviderの定義:

    • StreamProvider は非同期なストリームからデータを提供するための Provider です。この場合、 fetchData 関数を使用して非同期ストリームを提供しています。
  • MyHomePageクラス:

    • ConsumerWidget を継承し、StreamProviderが提供する非同期なストリームを ref.watch(streamProvider) を使用して取得し、 AsyncValue のwhenメソッドを使用して非同期データの状態に応じてUIを構築しています。
    • データが読み込み中なら CircularProgressIndicator 、エラーが発生したらエラーメッセージ、データが利用可能ならそのデータが表示されます。

Provider修飾子とメソッド

family

外部からパラメータを渡してデータを作成するようにしてくれる修飾子です。

作成したプロバイダに .family 修飾子を付けると、パラメータが追加され、追加されたパラメータはプロバイダのステート(状態)を計算する要素として使用することができます。

以下は.family 修飾子を使用する場面の例です。

  • FutureProvider と family を組み合わせて、メッセージ ID から Message を取得する場合
  • プロバイダにアプリの現在のロケール情報を渡して、翻訳文を取得する場合
メッセージ ID から Message を取得する例
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
  return dio.get('http://my_api.dev/messages/$id');
});

上記の family プロバイダをウィジェットなどで使用する場合、構文は通常のものと若干異なります。次の構文ではコンパイル時エラーが発生してしまいます。

コンパイルエラー発生
Widget build(BuildContext context, WidgetRef ref) {
  // エラー – messagesFamily はプロバイダではありません
  final response = ref.watch(messagesFamily);
}

代わりに、次のように messagesFamily に引数を渡してください。

Widget build(BuildContext context, WidgetRef ref) {
  final response = ref.watch(messagesFamily('id'));
}

.family 修飾子を使用するプロバイダに、異なる引数を同時に渡すことも可能です。例えば以下のように messagesFamily プロバイダに異なるid情報を渡し、それぞれのresponseを取得することもできます。

Widget build(BuildContext context, WidgetRef ref) {
  final response = ref.watch(messagesFamily('id'));
  final response1 = ref.watch(messagesFamily('id1'));
}

.family 修飾子はプロバイダに複数のオブジェクトを渡すことができません。
しかし、プリミティブ型(bool/int/double/String)か、定数(プロバイダ)もしくは == と hashCode をオーバーライドしたイミュータブル(不変)オブジェクトのいずれかである限りは どのような オブジェクトでも渡すことができます。以下はサンプルコードです。

サンプルコード

@freezed
abstract class MyParameter with _$MyParameter {
  factory MyParameter({
    required int userId,
    required Locale locale,
  }) = _MyParameter;
}

final exampleProvider = Provider.autoDispose.family<Something, MyParameter>((ref, myParameter) {
  print(myParameter.userId);
  print(myParameter.locale);
});

@override
Widget build(BuildContext context, WidgetRef ref) {
  int userId; // ユーザ ID をどこかで取得する
  final locale = Localizations.localeOf(context);

  final something = ref.watch(
    exampleProvider(MyParameter(userId: userId, locale: locale)),
  );

  ...
}

autoDispose

参照されなくなった Provider のデータを破棄してくれる修飾子です。

Firebase 使用時に、サービスとの接続を切って不必要な負荷を避けたり、ユーザが別の画面に遷移してまた戻って来る際に、ステートをリセットしてデータ取得をやり直す際などに使用されます。

以下のコードは .autoDispose の使用例です。 .autoDispose 修飾子は他の修飾子と組み合わせることもできます。

autoDispose使用例
final userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) {
 ...
});

ref.keepAlive

プロバイダに .autoDispose 修飾子を付けると、 ref オブジェクトに keepAlive というメソッドが追加されます。

keepAlive メソッドを実行することで、プロバイダが参照されなくなった際にもステートを維持するように設定することができます。

例えば、以下のように HTTP リクエスト完了後に keepAlive を実行するとします。

final myProvider = FutureProvider.autoDispose((ref) async {
  final response = await httpClient.get(...);
  ref.keepAlive();
  return response;
});

すると、リクエスト完了前に画面を破棄して再度同じ画面に戻った場合は HTTP リクエストが再実行される一方、 リクエスト完了後に同じ動作を行った場合はステートが維持されるため、HTTP リクエストが再実行されることはありません。

ref.onDispose

ref.onDispose を使用すると、プロバイダが参照されなくなったタイミング(プロバイダのステートを監視するオブジェクトがなくなったタイミング)で 処理を追加することができます。以下はリクエスト完了前に画面を離れてステートが破棄されたら、HTTP リクエストをキャンセルするサンプルコードです。

ref.onDisposeサンプルコード
final myProvider = FutureProvider.autoDispose((ref) async {
  // http リクエストのキャンセルを実行するための package:dio のオブジェクト
  final cancelToken = CancelToken();
  // プロバイダのステートが破棄されたら http リクエストをキャンセル
  ref.onDispose(() => cancelToken.cancel());

  // データを取得しつつキャンセル用の `cancelToken` を渡す
  final response = await dio.get('path', cancelToken: cancelToken);
  // リクエストが成功したらステートを維持する
  ref.keepAlive();
  return response;
});

Consumer と HookConsumer ウィジェット

ref オブジェクトは Consumer もしくは HookConsumer ウィジェットのコールバック関数 builder から取得することもできます。

これらのウィジェットを使用する場合は ConsumerWidget または HookConsumerWidget のようにクラスを定義する必要がありません。以下は HookConsumer を使用した例になります。

HookConsumer使用例
Scaffold(
  body: HookConsumer(
    builder: (context, ref, child) {
      final state = useState(0);
      final counter = ref.watch(counterProvider);
      return Text('$counter');
    },
  ),
);

ref.select

プロバイダを監視する対象を限定したい場合に使用します。例えば、特定の状態しか更新しない場合などに使用することで、不要な更新を避けることができます。

プロバイダを監視するということは、そのプロバイダが公開するオブジェクト全体の状態を監視するということです。 しかし、その監視の範囲を狭めて特定のプロパティのみを監視対象としたい場合があります。

例えば、以下のような User オブジェクトを公開するプロバイダがあるとします。

abstract class User {
  String get name;
  int get age;
}

それを監視するウィジェット側は username プロパティしか使用しません。

Widget build(BuildContext context, WidgetRef ref) {
  User user = ref.watch(userProvider);
  return Text(user.name);
}

上記のように userProvider を普通に監視すると、 無関係な age プロパティの変化もウィジェット更新のトリガーとなってしまいます。監視対象を name プロパティに限定したい場合は、 select を使用してその旨を明示的に宣言します。変更後のコードは以下のようになります。

Widget build(BuildContext context, WidgetRef ref) {
  String name = ref.watch(userProvider.select((user) => user.name));
  return Text(name);
}

ref.read

取得時点での状態を取得するのに使用します。状態の監視は行いません。
onTapinitState などのイベント処理でメソッドを使用する際などに使われます。

ref.readbuild メソッドで使用しない

ウィジェットのパフォーマンスを最適化するため、 ref.read を以下のように使うことがあるかもしれません。

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

Widget build(BuildContext context, WidgetRef ref) {
  final counter = ref.read(counterProvider.notifier);
  return ElevatedButton(
    onPressed: () => counter.state++,
    child: const Text('button'),
  );
}

しかし上記のようなコードは追跡困難なバグの要因になり得ます。プロバイダが公開する値は変わることがないため、 ref.read の使用は安全であると思うかもしれませんが、 常にプロバイダの値が変わらないということは保証できません。

変わらないと考えられていた値が、変更された場合、値の取得に ref.read を使っていた部分をすべて ref.watch に変更する必要があります。これはエラー発生の原因になり、変更し忘れる箇所がいくつか出てくる可能性もあります。

一方、最初から ref.watch を使用していれば、リファクタリング時に生じる問題は比較的少なくなるため、ほとんどの場面では watchlisten の使用、特に watch の使用がベターだと考えられています。

ウィジェットの更新回数を抑えるために ref.read を使いたい場合であっても、 ref.watch を使用してまったく同じ効果を得ることが可能です。

例えば

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

Widget build(BuildContext context, WidgetRef ref) {
  StateController<int> counter = ref.read(counterProvider.notifier);
  return ElevatedButton(
    onPressed: () => counter.state++,
    child: const Text('button'),
  );
}

とする代わりに、

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

Widget build(BuildContext context, WidgetRef ref) {
  StateController<int> counter = ref.watch(counterProvider.notifier);
  return ElevatedButton(
    onPressed: () => counter.state++,
    child: const Text('button'),
  );
}

としてください。 この2つのコードの効果はいずれも同じであり、カウンターの数字が増えた際にボタンが更新されることはありません。加えて、後者のアプローチの場合はカウンターの数字がリセットされた場合にも対応することができます。

例えば、以下のコードように別の場所で ref.refresh メソッドを使用して StateController オブジェクトを再度生成するとします。

もしここで ref.read を使用していた場合、ボタンは古い StateController インスタンスを使うことになり、このインスタンスはこの時点で既に破棄されているため、使うべきではないはずです。 一方 ref.watch を使用した場合は、ボタンが更新されて新しい StateController を取得することができます。

ref.refresh(counterProvider);

ref.watch

状態を取得し、監視を続けるために使用します。値に変更があれば、画面更新が行われるため、Widgetにリアクティブな値を埋め込む場合などに使用します。

ref.listen

ref.watch と同様にプロバイダを監視することができます。

最も大きな違いは、 ref.watch が値の変化に応じてウィジェットやプロバイダを更新するのに対して、 ref.listen は任意の関数を呼び出すことができます。エラー発生時のスナックバー表示など、何かしらの変化に反応して処理を実行したいときに使用します。

ref.listen メソッドは第1引数にプロバイダ、第2引数にステート(状態)が変化した際に実行するコールバック関数を設定します。 このコールバック関数には呼び出し時に、プロバイダの直前のステートと新しいステートの値が渡されるため、それぞれをパラメータとして使用できます。以下は listen メソッドを使用したサンプルコードです。

final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
      print('The counter changed $newCount');
    });
    
    return Container();
  }
}

まとめ

Riverpodとは

RiverpodはFlutterアプリケーションのための状態管理ライブラリで、柔軟性と拡張性が向上したProviderパッケージの改良版です。グローバルに定義されたプロバイダーを使用して、ウィジェットツリー内でデータを渡します。

基本的な使い方

1. ProviderScopeをルートに追加:

Riverpodを使用する際は、まず初めに ProviderScope をアプリのルートで追加します。

2. Providerをグローバル変数として定義:

Riverpodのプロバイダーはグローバルに定義され、手動で定義するかコード生成を使用して作成します。

3. Providerからデータを取得する:

ConsumerWidget を使用して、 build() メソッド内で WidgetRef 型の引数を利用してプロバイダーからデータを取得します。

便利な機能

  • スコープの明確化:
    Riverpodはプロバイダーのスコープを厳密に定義し、異なるスコープを持たせることができるため、依存関係の制御が容易になっています。

  • リアクティブなデータ変更:
    リアクティブな変更通知をサポートしているため、データが変更された際に関連するウィジェットを自動的に再構築することができます。

  • 様々なProvider:
    Providerだけでなく、FutureProvider、StreamProviderなど異なるデータソースに対する様々なプロバイダーが提供されています。

  • Null Safety:
    Null Safetyをサポートしているため、安全性と安定性が確保されています。

参考資料

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?