素人のアプリ開発日記

Androidアプリ開発について紹介します

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

お久しぶりです。今回は前回に引き続きriverpodを読んでいたので、メモ代わりに意訳をまとめました。

今回まとめたのは、以下の二つです。

riverpod.dev

riverpod.dev

Providerをreadする

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