🌀

Riverpodの使い方まとめとく

2022/12/05に公開

前準備

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.1.1

main.dart

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

以下はプロバイダーとは何かについてドキュメントの引用。プロバイダーの値を変更、監視することで状態管理する

プロバイダとは、状態の一部をカプセル化し、その状態をリッスンできるようにするオブジェクトのことである。

ConsumerとWidget

プロバイダーにアクセスするために必要なWidgetRefインスタンスを取得するためにComsumer ConsumerWidget ConsumerStatefulWidgetを使用する

Comsumer

任意の場所でプロバイダーを使いたい場合に使用

final fooProvider = Provider((ref) => "foo");

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

  
  Widget build(BuildContext context) => Consumer(
    builder: (context, WidgetRef ref, child) => Text(
      ref.watch(fooProvider),
    ),
  );
}

ConsumerWidget

StatelessWidgetWidgetでプロバイダーを使いたい場合に使用。Consumerでラップせずに使うとWidget全体が再ビルドの対象になる点に注意する

final fooProvider = Provider((ref) => "foo");

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

  
  Widget build(context, WidgetRef ref) => Text(
    ref.watch(fooProvider),
  );
}

ConsumerStatefulWidget

StatefulWidgetWidgetでプロバイダーを使いたい場合に使用。Consumerでラップせずに使うとWidget全体が再ビルドの対象になる点に注意する。また、initStateなどのライフサイクルメソッド内でプロバイダーの値にアクセスできる

final fooProvider = Provider((ref) => "foo");

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

  
  FooWidgetState createState() => FooWidgetState();
}

class FooWidgetState extends ConsumerState<FooWidget> {
  
  void initState() {
    super.initState();

    ref.read(fooProvider); // foo
  }

  
  Widget build(BuildContext context) => Text(
    ref.watch(fooProvider),
  )
}

プロバイダーへのアクセス

ref.watch

値の変化を監視する。値が変化したらWidgetが再ビルドされる

Consumer(
  builder: (context, WidgetRef ref, child) => Text(
    ref.watch(fooProvider),
  ),
);

ref.read

値の変化を監視しない

Consumer(
  builder: (context, ref, child) => TextButton(
    onPressed: () => ref.read(fooProvider.notifier).state++,
  ),
);

ref.listen

値の変化を監視する。値が変化してもWidgetの再ビルドはされない

Consumer(
  builder: (context, WidgetRef ref, child) {
    ref.listen(fooProvider, (previous, next) {
        // fooProviderの値が変化すると呼ばれる
    });
    ・・・
  },
);

select

再ビルドの条件を制御する。以下の例ではFoo.barのみを監視する。Foo.bazの値が変わっても再ビルドは行われない

class Foo {
  final int bar, baz;
  const Foo(this.bar, this.baz);
}

final fooProvider = StateProvider<Foo>((ref) => const Foo(0, 0));

・・・

Consumer(builder: (context, ref, child) {
  var foo = ref.watch(fooProvider.select((foo) => foo.bar));
  return Text("$foo");
}),

プロバイダーから他のプロバイダーへのアクセス

barProviderfooProviderを監視、FooWidgetbarProviderを監視。fooProviderの値が変わるとWidgetが再ビルドされる

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

final barProvider = StateProvider((ref) => ref.watch(fooProvider) * 2);

・・・

class FooWidget extends StatelessWidget {
  
  Widget build(BuildContext context) => Consumer(
    builder: (context, ref, child) => Text(ref.watch(barProvider)),
  );
}

プロバイダーの種類

Provider

プロバイダーの利用者に値の操作はさせず、参照のみをさせたい場合に使用する

final fooProvider = Provider((ref) => "foo");

class FooWidget extends StatelessWidget {
  
  Widget build(BuildContext context) => Consumer(
    builder: (context, ref, child) => Text(ref.watch(fooProvider)),
  );
}

StateProvider

プロバイダーの利用者に値の操作と参照をさせたい場合に使用する。また、値の操作方法を利用者で決めることができる

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

class FooWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    ・・・
    Consumer(
      builder: (context, ref, child) => "${Text(ref.watch(fooProvider))}",
    ),
    ・・・
    Consumer(
      builder: (context, ref, child) => TextButton(
        onPressed: () => ref.read(fooProvider.notifier).state++,
        ・・・
      ),
    ),
  }
}

StateNotifierProvider

プロバイダーの利用者に値の操作と参照をさせたい場合に使用する。ただし、値の操作方法は提供者が公開しているメソッドを使用する


class Foo {
  final int value;
  const Foo(this.value);
}

class FooController extends StateNotifier<Foo> {
  FooController() : super(const Foo(0));

  void increment() {
    state = Foo(state.value + 1);
  }
}

final fooProvider = StateNotifierProvider<FooController, Foo>((ref) => FooController());

class FooWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    ・・・
    Consumer(
      builder: (context, ref, child) => Text(
        "${ref.watch(fooProvider).value}",
      ),
    ),
    ・・・
    Consumer(
      builder: (context, ref, child) => TextButton(
        onPressed: () => ref.read(fooProvider.notifier).increment();,
        ・・・
      ),
    ),
  }
}

また、StateNotifierで管理する状態はイミュータブルな前提のため、以下のようなコードはref.watchで監視しても変更を検知できない

class Foo {
  int value;
  Foo(this.value);
}

class FooController extends StateNotifier<Foo> {
  ・・・
  void increment() => state.value++;
}

以下はref.watchの説明の翻訳を引用

プロバイダが公開する値を返し、その値が変更されたときにウィジェットを再構築します。値が変更されたらウィジェットを再構築する。

上の例ではFoo.valueの値を直接いじってもオブジェクトの参照先が変わらないので変更されたことにならない

ChangeNotifierProvider

プロバイダーの利用者に値の操作と参照をさせたい場合に使用する。ただし、値の操作方法は提供者が公開しているメソッドを使用する。StateNotifierProviderのミュータブルな値を管理したい版

class Foo {
  int value;
  Foo(this.value);
}

class FooController extends ChangeNotifier {
  Foo foo;
  FooController() : foo = Foo(0);

  void increment() {
    foo.value++;
    notifyListeners();
  }
}

final fooProvider = ChangeNotifierProvider((ref) => FooController());

notifyListeners()を呼ぶことで変更を通知する

FutureProvider

プロバイダーの利用者に値の操作はさせず、参照のみをさせたい場合に使用する。Providerの非同期操作が可能なことと、loading/errorステータスが簡単に扱える

final fooProvicer = FutureProvider<String>((ref) async {
  await Future.delayed(const Duration(seconds: 3));
  return "foo";
});

class FooWidget extends StatelessWidget {
  
  Widget build(BuildContext context) => Consumer(
    builder: (context, ref, child) => ref.watch(fooProvicer).when(
      data: (foo) => Text(foo),
      error: (err, st) => const Text("Error"),
      loading: () => const Text("Loading"),
    ),
  );
}

また、以下はドキュメントの翻訳の引用

FutureProviderはユーザのインタラクションの後に計算を直接修正する方法を提供しません。これは、単純なユースケースを解決するために設計されている。より高度なシナリオの場合は、StateNotifierProviderの使用を検討してください。

StreamProvider

プロバイダーの利用者に値の操作はさせず、参照のみをさせたい場合に使用する。FutureProviderStream

final fooProvider = StreamProvider((ref) {
  final controller = StreamController();
  controller.add("hello");
  return controller.stream;
});

class FooWidget extends StatelessWidget {
  ・・・
}

familyとautoDispose

family

プロバイダーに引数を渡すことができる

final fooProvider = StateProvider<int>((ref) => 0);

final barProvider = StateProvider.family<int, int>((ref, v)
  => ref.watch(fooProvider) * v);

・・・

Consumer(
  builder: (context, ref, child) => Text("${ref.watch(barProvider(100))}"),
),

autoDispose

使われなくなったプロバイダーを自動で削除する。また、onDisposeで削除時のコールバックを指定できる。例えばHTTPリクエストが完了する前にプロバイダーが削除される前にリクエストをキャンセルすることができる

final fooProvider = FutureProvider.autoDispose<Foo>((ref) async {
  // HTTPリクエストをキャンセルするのに使うトークン
  final token = CancelToken();

  // プロバイダーが削除されたら呼ばれるコールバックでHTTPリクエストをキャンセル
  ref.onDispose(() token.cancel());

  var res = await Dio().get('https://・・・', cancelToken: token);
  return Foo.fromResponse(res);
});

その他

overrides

プロバイダーを上書き

final fooProvider = StateProvider<int>((ref) => 0);

ProviderScope(
  overrides: [
    fooProvider.overrideWith((ref) => 100),
  ],
  child: Consumer(
    builder: (context, ref, child) => Text("${ref.watch(fooProvider)}"),
  ),
),

プロバイダーに別名をつける

final fooProvider = StateProvider.family<int, int>((ref, value) => value);

final barProvider = StateProvider<int>((ref) => throw UnimplementedError());

ProviderScope(
  overrides: [
    barProvider.overrideWith((ref) => ref.watch(fooProvider(100))),
  ],
  child: Consumer(
    builder: (context, ref, child) => Text("${ref.watch(barProvider)}"),
  ),
),

ref.refreshとref.invalidate

プロバイダーを初期状態に戻し再評価する

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

class FooWidget extends StatelessWidget {
  
  Widget build(BuildContext context) => Consumer(
    builder: (context, ref, child) => TextButton(
      child: Text("reset"),
      onPressed: () => ref.refresh(fooProvider)
    ),
  );
}

以下は、invalidaterefreshとの違いに関するドキュメントの引用

refreshとは対照的に、リフレッシュは即時ではなく、代わりに次の読み込みまたは次のフレームに遅れて行われます

Discussion