Open60

Rivorpodについての個人的なまとめ

Shikano YouheiShikano Youhei

Providerの欠点

  • コードの構造が複雑になりがち
  • Providerは依存性注入を簡単にはサポートしていない
    • コードの再利用とテストが少し難しくなる

Riverpodの長所

  • 依存性注入を簡単にサポート
  • タイプセーフ
  • コンパイル時にエラーをキャッチするのが容易
Shikano YouheiShikano Youhei

公式分

  • ProviderNotFoundExceptionやロード状態の処理忘れはもうありません。Riverpodを使用すると、コードがコンパイルされれば動作します。

  • RiverpodはProviderにインスパイアされているが、Providerの主要な問題点である、同じタイプの複数のプロバイダーのサポート、非同期プロバイダーの待ち受け、どこからでもプロバイダーの追加...などを解決している。

  • Flutterに依存することなく、プロバイダーを作成/共有/テストできる。これには、BuildContextなしでプロバイダをリッスンできることも含まれる。

Shikano YouheiShikano Youhei

ビルド・メソッド内でリストをソート/フィルターする必要も、高度なキャッシュ・メカニズムに頼る必要もなくなった。

Providerと "family "を使えば、本当に必要な時だけリストを並べ替えたり、HTTPリクエストを行うことができます。

Q: familyってなんだ?

final todosProvider = StateProvider<List<Todo>>((ref) => []);
final filterProvider = StateProvider<Filter>((ref) => Filter.all);

final filteredTodosProvider = Provider<List<Todo>>((ref) {
  final todos = ref.watch(todosProvider);
  switch (ref.watch(filterProvider)) {
    case Filter.all:
      return todos;
    case Filter.completed:
      return todos.where((todo) => todo.completed).toList();
    case Filter.uncompleted:
      return todos.where((todo) => !todo.completed).toList();
  }
});
Shikano YouheiShikano Youhei

プロバイダーを読んでも、決して悪い状態になることはない。プロバイダーを読み込むために必要なコードを書くことができれば、有効な値を得ることができる。

これは、非同期にロードされた値にも当てはまります。プロバイダを使用する場合とは対照的に、Riverpodでは、読み込み/エラーのケースをクリーンに処理できます。

final configurationsProvider = FutureProvider<Configuration>((ref) async {
  final uri = Uri.parse('configs.json');
  final rawJson = await File.fromUri(uri).readAsString();

  return Configuration.fromJson(json.decode(rawJson));
});

class Example extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final configs = ref.watch(configurationsProvider);

    // Use Riverpod's built-in support
    // for error/loading states using "when":
    return configs.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error $err'),
      data: (configs) => Text('data: ${configs.host}'),
    );
  }
}

ってかconfigって一般的にどんなもんを言うんだ?

→ 設定のことでした

Shikano YouheiShikano Youhei

Riverpodを使うことで、Flutterのdevtoolの中で、あなたの状態をすぐに見ることができる。
さらに、本格的なステートインスペクターも開発中だ。

これ便利だな

Shikano YouheiShikano Youhei

Providers

プロバイダは、Riverpodアプリケーションの最も重要な部分です。プロバイダは、状態の一部をカプセル化し、その状態をリッスンできるようにするオブジェクトです。

状態の一部をプロバイダーで包む:

  • 複数の場所でそのステートに簡単にアクセスできるようになる。プロバイダは、Singletons、Service Locators、Dependency Injection、InheritedWidgetsなどのパターンを完全に置き換えるものである。

  • このステートと他のステートとの結合を簡素化する。複数のオブジェクトを1つにまとめるのに苦労したことはないだろうか?このシナリオは、プロバイダー内部に直接構築されます。

  • パフォーマンスの最適化を可能にします。ウィジェットの再構築をフィルタリングする場合でも、高価なステート計算をキャッシュする場合でも、プロバイダは、ステートの変更によって影響を受けるものだけが再計算されるようにします。

  • アプリケーションのテスト容易性が向上します。プロバイダを使用すると、複雑な setUp/tearDown ステップが不要になります。さらに、任意のプロバイダをオーバーライドして、テスト中に異なる動作をさせることができるため、特定の動作を簡単にテストできます。

  • ロギングやpull-to-refreshなどの高度な機能と簡単に統合できます。

Shikano YouheiShikano Youhei

プロバイダーに渡された関数が返すオブジェクトの型は、使用するプロバイダーによって異なる。例えば、プロバイダーの関数は任意のオブジェクトを生成できる。一方、StreamProviderのコールバックはStreamを返すことが期待される。

Shikano YouheiShikano Youhei

プロバイダーには、さまざまなユースケースに対応する複数のタイプがある。

これらすべてのプロバイダが利用可能であるため、1つのプロバイダ・タイプを他のプロバイダ・タイプよりも使用するタイミングを理解するのが難しい場合があります。以下の表を使用して、ウィジェット・ツリーに提供したいものに適合するプロバイダを選択してください。

Provider Type Provider Create Function Example Use Case
Provider Returns any type サービス・クラス / 計算されたプロパティ (フィルタリングされたリスト)
StateProvider Returns any type フィルター条件 / 単純な状態オブジェクト
FutureProvider Returns a Future of any type APIコールの結果
StreamProvider Returns a Stream of any type APIからの結果のストリーム
StateNotifierProvider Returns a subclass of StateNotifier インターフェイスを通さない限り、不変である複雑な状態オブジェクト。
ChangeNotifierProvider Returns a subclass of ChangeNotifier ミュータビリティを必要とする複雑な状態オブジェクト
Shikano YouheiShikano Youhei

すべてのプロバイダには目的がありますが、ChangeNotifierProvidersはスケーラブルなアプリケーションには推奨されません。package:providerからの簡単な移行経路を提供するためにflutter_riverpodパッケージに存在し、Navigator 2パッケージとの統合などflutter特有のユースケースを可能にします。

Shikano YouheiShikano Youhei

すべてのプロバイダには、さまざまなプロバイダに追加機能を追加する方法が組み込まれています。

これらは、refオブジェクトに新しい機能を追加したり、プロバイダが消費される方法を少し変更したりします。修飾子は、名前付きコンストラクタに似た構文で、すべてのプロバイダに使用できます:

final myAutoDisposeProvider = StateProvider.autoDispose<int>((ref) => 0);
final myFamilyProvider = Provider.family<String, int>((ref, id) => '$id');

現時点では、2つの修飾子が利用可能です:

.autoDisposeは、プロバイダがリッスンされなくなったときに、その状態を自動的に破棄します。
.familyは、外部パラメーターからプロバイダーを作成できます。

Shikano YouheiShikano Youhei

てかほんなら毎回autoDipose着けなあかんのちゃうん
familyと一緒に使いたい時はどないすんねんな

Shikano YouheiShikano Youhei

何よりもまず、プロバイダを読み込む前に、"ref "オブジェクトを取得する必要がある。

このオブジェクトによって、ウィジェットであれ、他のプロバイダであれ、プロバイダと対話することができる。

Shikano YouheiShikano Youhei
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 can use the "ref" to read other providers
    final repository = ref.read(repositoryProvider);
    repository.post('...');
  }
}
Shikano YouheiShikano Youhei

このパラメータは、プロバイダが公開する値に渡しても安全である。

例えば、よくある使用例は、プロバイダの "ref "をStateNotifierに渡すことである

そうすることで、カウンター・クラスがプロバイダーを読むことができるようになる。

Shikano YouheiShikano Youhei

てかそもそもStateNotifierってRiverpod関係ないの?
(そこからかい)

Shikano YouheiShikano Youhei

ちょっと待ってくれ。公式ドキュメント日本語訳できるやん

Shikano YouheiShikano Youhei

ウィジェットには当然refパラメータはありません。しかし、Riverpodは、ウィジェットからrefパラメータを取得するための複数のソリューションを提供しています。

StatelessWidgetの代わりにConsumerWidgetを拡張する
ウィジェットツリーでrefを取得する最も一般的な方法は、StatelessWidgetをConsumerWidgetに置き換えることです。

ConsumerWidgetの使い方はStatelessWidgetと同じですが、唯一の違いはビルドメソッドに "ref "オブジェクトという追加のパラメータがあることです。

典型的なConsumerWidgetは以下のようになります:

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // use ref to listen to a provider
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}
Shikano YouheiShikano Youhei

StatefulWidget+Stateの代わりにConsumerStatefulWidget+ConsumerStateを拡張する
ConsumerWidgetと同様に、ConsumerStatefulWidgetとConsumerStateは、StatefulWidgetとそのStateに相当します。

今回、"ref "はビルド・メソッドのパラメータとして渡されるのではなく、ConsumerStateオブジェクトのプロパティとして渡されます:

Shikano YouheiShikano Youhei
class HomeView extends ConsumerStatefulWidget {
  const HomeView({super.key});

  
  HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
  
  void initState() {
    super.initState();
    // "ref" can be used in all life-cycles of a StatefulWidget.
    ref.read(counterProvider);
  }

  
  Widget build(BuildContext context) {
    // We can also use "ref" to listen to a provider inside the build method
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}
Shikano YouheiShikano Youhei

このオプションは flutter_hooks ユーザ向けです。flutter_hooks が動作するには HookWidget を拡張する必要があるので、フックを使うウィジェットは ConsumerWidget を拡張できません。

hooks_riverpod パッケージは HookConsumerWidget という新しいウィジェットを公開しています。HookConsumerWidgetは、ConsumerWidgetとしてもHookWidgetとしても動作します。これにより、ウィジェットはプロバイダをリッスンし、フックを使用することができます。

Shikano YouheiShikano Youhei
class HomeView extends HookConsumerWidget {
  const HomeView({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // HookConsumerWidget allows using hooks inside the build method
    final state = useState(0);

    // We can also use the ref parameter to listen to providers.
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}
Shikano YouheiShikano Youhei

HookWidgetの代わりにStatefulHookConsumerWidgetを拡張する
このオプションは、フックに加えてStatefulWidgetのライフサイクルメソッドを使用する必要があるflutter_hooksユーザーのためのものです。

Shikano YouheiShikano Youhei
class HomeView extends StatefulHookConsumerWidget {
  const HomeView({super.key});

  
  HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
  
  void initState() {
    super.initState();
    // "ref" can be used in all life-cycles of a StatefulWidget.
    ref.read(counterProvider);
  }

  
  Widget build(BuildContext context) {
    // Like HookConsumerWidget, we can use hooks inside the builder
    final state = useState(0);

    // We can also use "ref" to listen to a provider inside the build method
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}
Shikano YouheiShikano Youhei

ConsumerとHookConsumerウィジェット
ウィジェット内で "ref" を取得する最後の方法は、Consumer/HookConsumer に頼ることです。

これらのクラスは、ConsumerWidget/HookConsumerWidget と同じプロパティを持つ、ビルダーコールバックで "ref" を取得するために使用できるウィジェットです。

このように、これらのウィジェットは、クラスを定義することなく "ref "を取得する方法です。例としては

Shikano YouheiShikano Youhei
Scaffold(
  body: HookConsumer(
    builder: (context, ref, child) {
      // Like HookConsumerWidget, we can use hooks inside the builder
      final state = useState(0);

      // We can also use the ref parameter to listen to providers.
      final counter = ref.watch(counterProvider);
      return Text('$counter');
    },
  ),
);
Shikano YouheiShikano Youhei

refを使ってプロバイダーとやりとりする
ref "が手に入ったので、早速使ってみよう。

ref "には、主に3つの使い方がある:

  • プロバイダの値を取得し、その値が変更されたときに、その値を購読しているウィジェットやプロバイダをリビルドします。これは、ref.watch を使って行われます。
  • プロバイダにリスナーを追加して、プロバイダが変更されるたびに、新しいページに移動したり、モーダルを表示したりするアクションを実行します。
    これは、ref.listenを使って行います。
  • 変更を無視してプロバイダの値を取得します。これは、"on click "のようなイベントでプロバイダの値が必要な場合に便利です。これはref.readを使って行います。
Shikano YouheiShikano Youhei

FutureBuilderのデメリットってなんだろう。AsyncValueを使った方がいい理由ってなんだ?

Shikano YouheiShikano Youhei

ConsumerWidgetの中で定義されたWidgetにはWidgetRefを渡すことができると考えていい、、、のかな?

Shikano YouheiShikano Youhei

可能な限り、機能を実装する際にはref.readやref.listenよりもref.watchを使うことを好む。
ref.watchに頼ることで、アプリケーションはリアクティブかつ宣言的になり、保守性が高まる。

Shikano YouheiShikano Youhei

プロバイダを監視するために ref.watch を使う
ref.watchは、ウィジェットのビルドメソッド内またはプロバイダのボディ内で使用され、ウィジェット/プロバイダがプロバイダをリッスンします:

例えば、プロバイダはref.watchを使って複数のプロバイダを新しい値にまとめることができます。

たとえば、プロバイダは ref.watch を使って複数のプロバイダを新しい値にまとめることができます。2つのプロバイダを持つことができる:

filterTypeProvider:現在のフィルタータイプ(なし、完了したタスクのみ表示、...)を公開するプロバイダー。
todosProvider: タスクのリスト全体を公開するプロバイダ。
そして、ref.watchを使うことで、両方のプロバイダを組み合わせて、フィルタリングされたタスクのリストを作る第3のプロバイダを作ることができる:

Shikano YouheiShikano Youhei
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((ref) {
  // obtains both the filter and the list of todos
  final FilterType filter = ref.watch(filterTypeProvider);
  final List<Todo> todos = ref.watch(todosProvider);

  switch (filter) {
    case FilterType.completed:
      // return the completed list of todos
      return todos.where((todo) => todo.isCompleted).toList();
    case FilterType.none:
      // returns the unfiltered list of todos
      return todos;
  }
});
Shikano YouheiShikano Youhei

このコードで、filteredTodoListProvider はフィルタリングされたタスクのリストを公開するようになりました。

フィルターされたリストは、フィルターかタスクのリストのどちらかが変更された場合、自動的に更新されます。同時に、フィルターもタスクリストも変更されなければ、フィルターされたリストは再計算されません。

同様に、ウィジェットは ref.watch を使ってプロバイダからのコンテンツを表示し、そのコンテンツが変更されるたびにユーザーインターフェイスを更新することができます:

Shikano YouheiShikano Youhei
final counterProvider = StateProvider((ref) => 0);

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // use ref to listen to a provider
    final counter = ref.watch(counterProvider);

    return Text('$counter');
  }
}
Shikano YouheiShikano Youhei

このスニペットは、カウントを保存するプロバイダをリッスンするウィジェットを示している。カウントが変更されると、ウィジェットは再構築され、UIは新しい値を表示するように更新されます。

Shikano YouheiShikano Youhei

ウォッチ・メソッドは、ElevatedButtonのonPressedのように非同期に呼び出すべきではありません。また、initStateやその他のStateのライフサイクルの中で使うべきではありません。

そのような場合は、代わりにref.readを使用することを検討してください。

Shikano YouheiShikano Youhei

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

両者の主な違いは、リッスンされたプロバイダが変更された場合にウィジェット/プロバイダを再構築するのではなく、ref.listen を使用するとカスタム関数が呼び出されることです。

これは、エラーが発生したときにスナックバーを表示するなど、特定の変更が発生したときにアクションを実行するのに便利です。

ref.listenメソッドには2つの位置引数が必要で、1つ目はプロバイダ、2つ目は状態が変化したときに実行したいコールバック関数です。コールバック関数が呼び出されると、2つの値が渡されます。前のStateの値と新しいStateの値です。

ref.listenメソッドは、プロバイダ本体の内部で使用することができる:

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

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

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

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

ref.readメソッドは、プロバイダの状態をリッスンせずに取得する方法です。

一般的には、ユーザーとのインタラクションをトリガーとする関数の内部で使用されます。例えば、ユーザーがボタンをクリックしたときにカウンターをインクリメントするためにref.readを使うことができます:

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

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Call `increment()` on the `Counter` class
          ref.read(counterProvider.notifier).increment();
        },
      ),
    );
  }
}
Shikano YouheiShikano Youhei

リッスンしたいプロバイダーによっては、リッスン可能な値が複数ある場合がある。

例として、以下のStreamProviderを考えてみよう:

final userProvider = StreamProvider<User>(...);
Shikano YouheiShikano Youhei

このuserProviderを読むとき、次のことができる:

userProvider自身をリッスンすることによって、現在の状態を同期的に読む:

Shikano YouheiShikano Youhei
Widget build(BuildContext context, WidgetRef ref) {
  AsyncValue<User> user = ref.watch(userProvider);

  return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
  );
}
Shikano YouheiShikano Youhei

select "を使った再構築のフィルタリング
プロバイダの読み込みに関連する最後の機能として、ref.watch からウィジェット/プロバイダを再構築する回数、つまり ref.listen が関数を実行する回数を減らす機能があります。

デフォルトでは、プロバイダをリッスンすると、オブジェクトの状態全体をリッスンするので、これは覚えておくと重要です。しかし、ウィジェット/プロバイダが、オブジェクト全体ではなく、一部のプロパティの変更だけを気にすることもあります。

例えば、プロバイダは、User:

abstract class User {
  String get name;
  int get age;
}
Shikano YouheiShikano Youhei

selectを使用することで、気になるプロパティを返す関数を指定することができます。

ユーザーが変更されるたびに、Riverpodはこの関数を呼び出し、以前の結果と新しい結果を比較します。両者が異なる場合(名前が変更された場合など)、Riverpodはウィジェットを再構築します。
しかし、それらが等しい場合(年齢が変更された場合など)、Riverpodはウィジェットを再構築しません。