torikatsu.dev

Flutterとかプログラミングとかガジェットとか書きます

Riverpodのドキュメントを意訳してみた1

こんにちは。 私は、現在FlutterアプリにRiverpod導入を考えています。導入に先立ってRiverpodの公式ドキュメントを読みました。 いつかドキュメント全体をざっくり見返すときにあまり頭を使いたくないため、メモの意味も兼ねてドキュメントを意訳して記事にまとめておくことにしました。

今回、意訳したページは以下のリンク先です。

意訳は正確でないこともあるのであらかじめご了承ください。

riverpod.dev

Providerをなぜ使うのか

  • 様々な場所から状態へのアクセスを簡単にする
  • 状態の組み合わせを単純にする
  • パフォーマンスを最適化する
  • テスタビリティを向上させる
  • 簡単に統合する

Providerの生成

Providerは異なった方法で生成されるが動作は一緒 一般的に以下のように、グローバル定数として宣言される

final myProvider = Provider((ref) {
  return MyValue();
});

Note

Providerをグローバル変数で宣言することを恐れる必要はない
Providerは完全に変更不可能だ。Providerを宣言することは関数を宣言することと変わらない、そしてテストやメンテナンスをしやすい

このスニペットは3要素から成り立っている - final myProviderという変数宣言 将来的にProviderから状態を読み込むための変数で、つねに変更不可能だ。

  • Provider、使用するProviderのp種類 Providerは一番基本的なProviderだ。二度と変更されないオブジェクトを公開する。状態をインタラクティブに変更するためにProviderStreamProviderStateNotifierProviderに変更することもできる。

  • 状態を共有する関数 関数は常にrefとよばれるオブジェクトを引数で受け取る。このオブジェクトはほかのProviderを読み取ることや、Providerが破棄されるときになんらかの処理を実行することを可能にする(フック的なやつ)。

関数によって作られたオブジェクトの型は、使用されるProviderに依存したProviderに渡される。 例えば、Providerの関数はなんらかのオブジェクトを生成できる。一方、StreamProviderのコールバックはStreamを返すだろう。

Info

宣言可能なProviderはどれも制限を必要としない。package:providerを使用した場合とは逆に、Riverpodでは同じ型のProviderを公開できる。 実際、ふたつのProviderはどちらもStringを生成しているが、問題はない。

Caution

Providerを使用する時、ProviderScopeをFlutterApplicationsのルートに追加してください。

Stateが破棄される前の処理の実行について

時には、状態についてのProviderは破棄・再生成される。一般的な使用例は、providerが破棄される前に状態を破棄する。たとえばStreamControllerをcloseするように。

これはref.onDispose()を使用することで実現する。

final example = StreamProvider.autoDispose((ref) {
  final streamController = StreamController<int>();
  ref.onDispose(() {
    streamController.close();
  });
  return streamController.stream;
}

Note

使用するProviderによっては、事前にProviderを破棄する方法が用意されているかもしれない。 たとえば、StateNotifierProviderStateNotifierdisposeメソッドを呼ぶ

ProviderのModifierについて

すべてのProviderには、さらなる機能をついかする方法が用意されている。 refに新しい特徴を追加するか、ProviderをConsumeする方法を少し変更している。

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

ときには、二種類のModifiersを使用することが可能だ

  • .autoDispose, 参照されなくなった時にProviderの状態を自動で破棄する
  • .family, 外部のパラメータからProviderの生成を可能にする。

Note

はじめに複数のmodifiersを使う時

final userProvider = FeatureProvider.autoDispose.family<User, int>(ref, userId) async {
  return fetchUser(userId);
});

Providerをreadする

このガイドを読む前にread about Providersを読んね

このガイドでは、どうやってProviderを使用するのかを知ることができる

providerをreadする方法はいくつかあって、基準・規格・仕様によって少し違う 手短に知るには以下の図にそってどのproviderをreadするかみてね

どこでProviderをreadするか? - テストやDart-Only-PackageでreadするならProviderContainer.read

  • Widgetの中

    • Dialog表示のためにlisteningするならProviderListener
    • onPressedを覗くbuildメソッドの中でreadしなければContext.read
    • flutter-hooksを使うならuseProvider
    • flutter-hooksを使わないならConsumer
  • 他のProviderの中

    • providerの更新に伴ってproviderによって公開された値が、再生成されるべきか
      • べきならProviderReference.watch
      • ちがうならproviderReference.read

つぎにそれぞれ個別の場合をみながらどうやって動作するのか紹介します

このガイドでは以下のproviderで考えます。

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

readの方法を決定する

listenしたいProviderによって、値をlistenする方法がいくつかあります

たとえば、以下のStreamProviderについてだと、

final userProvider = Streamprovider<User>(...);

userProviderをreadするとき、以下のような方法が可能である。

  • userProvider自身で同期的に現在の状態をreadする
Widget build(BuildContext context, ScopeReader watch) {
  AsyncValue<User> user = watch(userProvider);

  return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
  );
}
  • Streamの連携に伴って変更される値をreadするならuserProvider.stream
Widget build(BuildContext context, ScopeReader watch) {
  Stream<User> user = watch(userProvider.stream);
}
  • 最新の値で発火されるFutureで解決される値を取得するには、userProvider.lastを使用する
Widget build(BuildContext context, Scopedreader watch) {
  Future<User> user = watch(userProvider.last);
}

さらなる情報はAPIリファレンスみてね

ProviderをWidgetの中で使う

この章では、ProviderをWidgetの中で使う方法について知ることができる

ConsumerWidget

ConsumerWidgetStatelessWidgetのような基底クラスだ。そして、providerをlistenすることができる

class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, Scopedreader watch) {
    int count = watch(counterProvider).state;

    return Scaffold(
      appBar: AppBar(title: const Text('Counter example'));
      body: Center(
        child: Text('$count'),
      ),
    );
  }
}

この例では、counterProviderとcounterが変更される旅にTextがリビルドされるだろう。

counterProviderの値によって気づくことは、関数がwatchを呼ぶことだ。ConsumerWidgetで作られた関数は私たちのproviderをlistenし、値の変更が公開されるとリビルドする。

Caution

ConsumerWidgetの引数を渡すwatch()は```onPressedのような関数では非同期で呼ばないべきだ。

もしユーザのイベントのレスポンスをproviderをreadする必要があるなr、myProvider.read(BuildContext)を使用してください

Consumer

Consumerはアプリケーション内でデータを扱う特定のウィジェットの再描画のパフォーマンスを最適化することができるConsumerWidget

たとえば、ConsumerWidgetのスニペットを更新以下のコードでは、まえもってTextのみが再描画されることを認識できる。

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter example')),
      body: Center(
        child: Consumer(
          builder: (context, watch, child) {
            int count = watch(counterProvider).state;
            return Text('$count');
          },
        ),
      ),
    );
  }
}

context.read(myProvider)

ときには、listenする値が存在しないこともある。例えば、オブジェクトはonPressedでのみ必要な場合。

私たちはConsumerを使うことができる

Consumer(builder: (context, watch, _) {
  StateController<int> counter = watch(counterProvider);
  return RaisedButton(
    onPressed: () => counter.state++,
    child: Text('increment');
  )
});

しかし、これは効果的ではない。providerのlistenによって、counterが変更される旅にRaisedButtonが再描画される。counterが実際にbuildに使われない場合でさえ。

解決方法としてcontext.read(myProvider)が存在する。

これを使うことで前述のコードを以下のようにリファクターできる。

@override
Widget build(BuildContext context) {
  retur RaisedButton(
    onPressed: () => context.read(counterProvider).state++,
    child: Text('increment'),
  );
}

こうすることで、依然として、クリックすることで、ボタンはcounterをインクリメントする。しかし、もはやproviderをlistenしておらず、不必要な再描画を避けている。

context.readがみつからない。どうしたらいい?

もしcontext.readが見つからないなら、それはおそらく正しいパッケージをインポートしていないからだ。このメソッドを使うにはpackage:flutter_riverpodか、package:hooks_riverpodをインポートしなければならない。

Info

listenしているProviderによっては、これは必要ないかもしれない。 たとえば、StateNotifierProviderはそれ自身の状態のlistenを除く、StateNotifierを取得する方法が用意されている。

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

  void increment() => state++;
}

final counterProvider = StateNotifierProvider((ref) => Counter());

// ...
@override
Widget build(BuildContext context, ScopedReader watch) {
  final Counter counter = watch(counterProvider);

  return RaisedButton(
    onPressed: counter.increment,
    child: Text('increment'),
  );
}

Caution

context.readbuild()の中で呼ぶことを回避してください。もし、再描画を最適化したいなら、値の変更のlistenをProviderの中に変更してください。

ProviderListener

ときには、Providerの変更のあとでダイアログの表示やウィジェットツリーへのルートの挿入を行いたい場合もある。

ProviderListenerウィジェットを利用して以下のように実装してください

Widget build(BuildContext context) {
  return ProviderListener<StateController<int>>(
    provider: counterProvider,
    onChange: (counter) {
      if(counter.state == 5) {
        show Dialog(...);
      }
    },
    child: Whatever(),
  )
}

これできっとカウントが5になるとダイアログが表示されます。

Providerの中で他のProviderをreadする

一般的なProviderの作成方法として他のオブジェクトからオブジェクトを生成することがある。 例えば、UserRepositoryからUserControllerを生成したい時、それらは違ったProviderによって公開されているとする。

このシナリオはproviderがパラメータを受け取るためのrefを使うことによって可能になる

final userRepositoryProvider = Provider((ref) => UserRepository());

final userControllerProvider = StateNotifierProvider((ref) {
  return UserController(
    repository: ref.watch(userRepositoryProvider),
  );
});

CombineProvidersガイドでより多くの情報を得られる。例えば以下のように - 時間と共に変化する値からオブジェクトを作る時、どんなことがおこるか - 良い方法について