2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

riverpod 公式ドキュメントまとめ

Posted at

RiverPod

公式はじめに | Riverpod

プロバイダについて

  • 状態管理を行うためのライブラリ(ツリーの中でバケツリレー的なステートの受け渡しをなくして、直接ステートを参照できるようにするもの)

    • プロバイダを使用する場合にはFlutterのアプリルートにProviderScopeを置く必要がある
void main() {
  runApp(ProviderScope(child: MyApp()));
}
  • プロバイダはグローバル定数として、宣言するのが一般的である
    • その際にイミュータブル(不変)であることを意識する(イメージ的には関数をグローバルに定義する感覚と同じ)

final myProvider = Provider((ref) {
  return MyValue();
});
  • final myProviderで変数を宣言する。この際にはfinalで宣言する
    • この変数を用いてステートを呼び出す
  • Provider((ref) {return MyValue();});で使用するプロバイダの種類を確定する
  • refオブジェクトをパラメータとして、他のプロバイダを使用したり、ステートが破棄される際のコールバック関数を登録したりする

プロバイダの種類

  • Provider (型は任意)
    • サービスクラス / 算出プロパティ(リストのフィルタなど)
  • StateProvider (型は任意)
    • フィルタの条件 / シンプルなステートオブジェクト
  • StateProvider (型はFuture)
    • APIの呼び出し結果
  • StreamProvider (型はStream)
    • API の呼び出し結果の Stream
  • StateNotifierProvider (StateNotifierのサブクラス)
    • イミュータブル(インタフェースを介さない限り)で複雑なステートオブジェクト
  • ChangeNotifierProvider (ChangeNotifierのサブクラス)
    * ミュータブルで複雑なステートオブジェクト(ミュータブル(可変)であり、特有のユースケースに対応する為に存在している)

プロバイダ修飾子

  • .autoDispose はプロバイダの監視が終わったタイミングで、プロバイダに自動でステートを破棄させることができるようになる
  • .family はプロバイダ外部の値を用いてプロバイダを作成できるようになる
final myAutoDisposeProvider = StateProvider.autoDispose<int>((ref) => 0);
final myFamilyProvider = Provider.family<String, int>((ref, id) => '$id');

// 同時利用も可能
final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
  return fetchUser(userId);
});

プロバイダの利用方法

  • プロバイダを利用するにはrefオブジェクトを取得する必要がある
    • ref はウィジェットもしくはプロバイダから取得することができる

プロバイダからの取得

  • プロバイダはすべて ref オブジェクトを引数として受け取る
final provider = Provider((ref) {
  // `ref` を通じて他のプロバイダを利用する
  final repository = ref.watch(repositoryProvider);

  return SomeValue(repository);
})
  • ref はさらに別のオブジェクトに渡すこともできる
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter(ref);
});

class Counter extends StateNotifier<int> {
  Counter(this.ref) : super(0);

  final Ref ref;

  void increment() {
    // Counter は `ref` を使って他のプロバイダーを利用することができる
    final repository = ref.read(repositoryProvider);
    repository.post('...');
  }
}

Widgetからの取得

  • 通常のウィジェットでは ref を使用することができないため、 Riverpod では複数の方法を用意しています。

StatelessWidget の代わりに ConsumerWidget を継承する

*  [ConsumerWidget](https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/ConsumerWidget-class.html)  は  [StatelessWidget](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html)  とほぼ同等のものであり、build メソッドに`ref`を渡す第2パラメータが存在する以外の違いはない。
class HomeView extends ConsumerWidget {
  const HomeView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // `ref` を使ってプロバイダーを監視する
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

StatefulWidget+State の代わりに ConsumerStatefulWidget+ConsumerState を継承する

class HomeView extends ConsumerStatefulWidget {
  const HomeView({Key? key}) : super(key: key);

  @override
  HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
  @override
  void initState() {
    super.initState();
    //  `ref` は StatefulWidget のすべてのライフサイクルメソッド内で使用可能です。
    ref.read(counterProvider);
  }

  @override
  Widget build(BuildContext context) {
    //  `ref` は build メソッド内で使用することもできます。
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

HookWidget の代わりに HookConsumerWidget を継承する

::flutter_hooks 向け::

class HomeView extends HookConsumerWidget {
  const HomeView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // build メソッド内でフックを使用できます。
    final state = useState(0);

    // `ref` オブジェクトを使ってプロバイダを監視することもできます。
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

HookWidget の代わりに StatefulHookConsumerWidget を継承する

class HomeView extends StatefulHookConsumerWidget {
  const HomeView({Key? key}) : super(key: key);

  @override
  HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
  @override
  void initState() {
    super.initState();
    // 「ref」は StatefulWidget のすべてのライフサイクルメソッド内で使用できます。
    ref.read(counterProvider);
  }

  @override
  Widget build(BuildContext context) {
    // HookConsumerWidget のように build メソッドの中でフックが使えます。
    final state = useState(0);

    // プロバイダ監視のために「ref」を使用することも可能です。
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

Consumer と HookConsumer ウィジェット

  • ref オブジェクトは Consumer もしくは HookConsumer ウィジェットのコールバック関数 builder から取得することもできる。
    これらのウィジェットを使用する場合は ConsumerWidget および HookConsumerWidget のようにクラスを定義する必要がない。
Scaffold(
  body: HookConsumer(
    builder: (context, ref, child) {
      // HookConsumerWidget と同様にフックが使えます。
      final state = useState(0);

      // `ref` オブジェクトを使ってプロバイダを監視することもできます。
      final counter = ref.watch(counterProvider);
      return Text('$counter');
    },
  ),
);

ref の使い方

  • 3通りある
    • ref.watch: プロバイダの値を取得した上で、その変化を監視する。値が変化すると、その値に依存するウィジェットやプロバイダの更新が行われる。
    • ref.listen: プロバイダの値を監視し、値が変化するたびに呼び出されるコールバック関数(画面遷移、ダイアログの表示など)を登録する。
    • ref.read: プロバイダの値を取得する(監視はしない)。クリックイベント等の発生時に、その時点での値を取得する場合に使用できる。
      機能の実装時には可能な限り ref.watch を使用すること。 ref.watch によりアプリはリアクティブかつ宣言的になり、コードの保守性を高まる。
      watch と ListenメソッドはElevatedButtonのonPressed 内など、非同期的な場面で呼び出さないこと。またinitState を始め、Stateのライフサイクルメソッド内での使用も避ける。これらの場合は代わりに ref.read を使用すること

ref.watch

  • ウィジェットあるいはプロバイダ内で ref.watch を使ってプロバイダを監視することができる

    • プロバイダ内での使用
      • ref.watch でプロバイダに別の複数のプロバイダを監視させ、それらの値を組み合わせて新たに値を生成するということも可能
// 複数のプロバイダを監視は Todo リストのフィルタリングにも活用できます。 例えば、Todo アプリに次のプロバイダがあるとします。
	//•	filterTypeProvider: 現在のフィルタの種類を公開するプロバイダ(なし、完了のみ表示、未完了のみ表示...)
	//•	todosProvider: Todo リストの内容をすべて公開するプロバイダ
	//•	filteredTodoListProvider: Todo リストの完了と未完了をフィルタリングするプロバイダ

// 現在のフィルタの種類を公開するプロバイダ(なし、完了のみ表示、未完了のみ表示...)
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);

// Todo リストの内容をすべて公開するプロバイダ
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

// リストの完了と未完了をフィルタリングするプロバイダ
final filteredTodoListProvider = Provider((ref) {
  // フィルタの種類と Todo リストを取得、監視する
  final FilterType filter = ref.watch(filterTypeProvider);
  final List<Todo> todos = ref.watch(todosProvider);

  switch (filter) {
    case FilterType.completed:
      //  Todo リストを完了タスクのみにフィルタリングして値を返す
      return todos.where((todo) => todo.isCompleted).toList();
    case FilterType.none:
      // フィルタ未適用の Todo リストをそのまま返す
      return todos;
  }
});

//filterTypeProvider や todosProvider の値が変わると自動的に更新されます。 逆に言えば、いずれかが変わらない限りは再計算されることはありません。
  • ウィジェット内での使用
final counterProvider = StateProvider((ref) => 0);

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // `ref` を使ってプロバイダを監視する
    final counter = ref.watch(counterProvider);

    return Text('$counter');
  }
}
//値が変わるたびに UI を更新させることができる

ref.listen

  • ref.listenref.watch と同様にプロバイダを監視することができるが、最大の違いはref.watch が値の変化に応じてウィジェットやプロバイダを更新するのに対して、 ref.listen は任意の関数を呼び出してくれるという点です。
    • 使用ケースとしては、エラー発生時のスナックバー表示など、何かしらの変化に反応して処理を実行したいときなど
  • ref.listen メソッドは2つの位置引数が必要である。 ::第1引数にプロバイダ::、::第2引数にステート(状態)が変化した際に実行するコールバック関数::を渡す。 このコールバック関数には呼び出し時に、プロバイダの直前のステートと新しいステートの値が渡されるため、それぞれをパラメータとして使用できる
    プロバイダ内
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));

final anotherProvider = Provider((ref) {
  ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
    print('The counter changed $newCount');
  });
  // ...
});

ウィジェット

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

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

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

ref.read

ref.read はリアクティブではないため、可能な限り使用を避けること
【重要】 ref.read は build メソッドの中で使わない。

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

Widget build(BuildContext context, WidgetRef ref) {
  // `ref.read` を使うことでプロバイダのステート変化は無視される
  final counter = ref.read(counterProvider.notifier);
  return ElevatedButton(
    onPressed: () => counter.state++,
    child: const Text('button'),
  );
}
//プロバイダが公開する値が変わることはない。だから ref.read の使用は安全であるという部分から使用すると、リアクティブにする変更をしなければならない瞬間に値が変化することをアプリケーションは受け入れなければならない。であれば、最初からリアクティブなwatchを使用するべきである。(以下公式)

//ソフトウェアに変更はつきものです。そして将来、絶対に変わらないと考えられていた値が、変わる必要に迫られることは十分あり得ることです。 そしてもしこの値の取得に ref.read が使われていたら、その時にはコードを振り返り ref.read が使われているところをすべて ref.watch に変更する必要があります。これはエラーを招くおそれがありますし、変更し忘れる箇所がいくつか出てくる可能性もあります。一方、最初から ref.watch を使用していれば、リファクタリング時に生じる問題は比較的少ないはずです。
  • ref.read メソッドを使うことでプロバイダのその時点でのステートを取得することができる。純粋な取得以外の副作用はない。
    • ref.read はユーザ操作によって呼び出される関数内で使用するのが一般的である。 例えば、ボタンクリックイベント発生時にカウンターの数字を変更する場合に使用できる
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // `Counter` クラスの `increment()` メソッドを呼び出す
          ref.read(counterProvider.notifier).increment();
        },
      ),
    );
  }
}

selectを使って更新の条件を限定する

  • プロバイダを監視するということは、そのプロバイダが公開するオブジェクト全体のステートを監視することであり、 その監視の範囲を狭めて特定のプロパティのみを監視対象としたい場合がある
// Userオブジェクト
abstract class User {
  String get name;
  int get age;
}

// nameプロパティしか関心がないが、 userProvider を普通に監視すると無関係な age プロパティの変化もウィジェット更新のトリガーとなってしまう
Widget build(BuildContext context, WidgetRef ref) {
  User user = ref.watch(userProvider);
  return Text(user.name);
}

// selectでnameプロパティだけに関心を絞る
Widget build(BuildContext context, WidgetRef ref) {
  String name = ref.watch(userProvider.select((user) => user.name));
  return Text(name);
}

// listenもOK
ref.listen<String>(
  userProvider.select((user) => user.name),
  (String? previousName, String newName) {
    print('The user name changed $newName');
  }
);
  • select を使って監視対象にするプロパティを返す関数を指定することができる。User オブジェクトに変化があるたびに、Riverpod はこの関数を呼び出し、対象プロパティの古い値と新しい値を比較する。 値が異なる場合は Riverpod はウィジェットを更新、 対象プロパティ以外の値が変わっただけで対象プロパティの値自体が不変の場合は、Riverpod はウィジェットの更新をしない

  • select で明示する値は、必ずしも対象オブジェクトのプロパティそのものである必要はない。 == 演算子のオーバーライドなどでオブジェクトの等価性が定義されていれば何を返しても問題はない

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));

プロバイダのステートを組み合わせる

  • 基本的にプロバイダを組み合わせる場合はrefオブジェクトのwatachメソッドを使用する
    • 状態を監視してくれるため、依存先のプロバイダの値が変更されたら、依存元のプロバイダも再評価してくれる
final cityProvider = Provider((ref) => 'London');


final weatherProvider = FutureProvider((ref) async {
  // `ref.watch` により他のプロバイダの値を取得・監視します。
  // 利用するプロバイダ(ここでは cityProvider)を引数として渡します。
  final city = ref.watch(cityProvider);

  // 最後に `cityProvider` の値をもとに行った計算結果を返します。
  return fetchWeather(city: city);
});


// ===============実践的===============
// Todoリストクラス
class TodoList extends StateNotifier<List<Todo>> {
  TodoList(): super(const []);
}
// Todoリスト外部公開するためのプロバイダ
final todoListProvider = StateNotifierProvider((ref) => TodoList());

// フィルタなし、完了、未完了のステータス
enum Filter {
  none,
  completed,
  uncompleted,
}
// フィルタのステータスを外部公開するためのプロバイダ
final filterProvider = StateProvider((ref) => Filter.none);
// Todoリストプロバイダとステータスプロバイダを組み合わせて、ステータスに応じたTodoリストを外部公開するプロバイダを作成
final filteredTodoListProvider = Provider<List<Todo>>((ref) {
  final filter = ref.watch(filterProvider);
  final todos = ref.watch(todoListProvider);

  switch (filter) {
    case Filter.none:
      return todos;
    case Filter.completed:
      return todos.where((todo) => todo.completed).toList();
    case Filter.uncompleted:
      return todos.where((todo) => !todo.completed).toList();
  }
});
// これで UI 側にこの filteredTodoListProvider を監視させることで、その値の変化に応じて Todo リストを表示することができるようになった。 フィルタの種類もしくは Todo リストの内容が変われば、UI も自動的に再構築される。

これで他のプロバイダの値に依存するプロバイダを作ることができた。

プロバイダオブザーバー

プロバイダのライフサイクル

  • didAddProvider
    • プロバイダが初期化されるたびに呼び出される。公開される値は value パラメータで利用可能
  • didDisposeProvider
    • プロバイダが破棄される度に呼ばれる
  • didUpdateProvider
    • プロバイダが変更通知を送信するたびに呼び出される

使用方法

  • ProviderObserver クラスを継承するクラスを定義し、使用したいメソッドをオーバーライドして使用する
    • 例としてidUpdateProvider メソッドをオーバーライドして、プロバイダのステート変化をログに残すという用途にも使用することができる
// Riverpod を使用した Logger 付きのカウンターアプリの例

class Logger extends ProviderObserver {
  @override
  void didUpdateProvider(
    ProviderBase provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
    print('''
{
  "provider": "${provider.name ?? provider.runtimeType}",
  "newValue": "$newValue"
}''');
  }
}

void main() {
  runApp(
    // ProviderScope を置くことで Riverpod が有効になる
    // Logger インスタンスを observers のリストに追加する
    ProviderScope(observers: [Logger()], child: const MyApp()),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: Home());
  }
}

final counterProvider = StateProvider((ref) => 0, name: 'counter');

class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Counter example')),
      body: Center(
        child: Text('$count'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

// ステートが変化する度にログ取るようになる
I/flutter (16783): {
I/flutter (16783):   "provider": "counter",
I/flutter (16783):   "newValue": "1"
I/flutter (16783): }
2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?