[Flutter] Riverpodの.familyで引数を親ごとに定義する方法
Vueで言うところのProvide、Reactで言うところのRecoilみたく、別々の親から呼び出された共通のComponentに固有の状態をそれぞれ渡す必要があった。
.familyを使えば引数にユニークな値を与えてやればよいが、ネストが深いと親から子まで.family用のkeyをひたすら渡し続けなければならなくなる。
ショートアンサー
Providerを追加して、親のbuild内でProviderScope(override をする。
final messageProviderFamily = ProviderFamily<Message, int>(((ref, messageId) {
return Message(message: messageRecord[messageId]!, id: messageId);
}));
final messageProvider = Provider<Message>(((ref) => const Message()));
このようにProviderを2つ定義して、
ProviderScope(
overrides: [
messageProvider.overrideWithProvider(
Provider(((ref) => ref.read(messageProviderFamily(1))))
),
],
child: const ContainerA(),
),
それぞれの親でoverrideに任意の値を渡してあげれば良い。
Bad(バケツリレー)
愚直にやると、このパターンになる。
ひたすら親から子に.familyのkeyをバケツリレーする。
class Message {
const Message({this.message = 'No Massage', this.id = 1});
final String message;
final int id;
}
/// 仮でDBみたいなもの
Map<int, String> messageRecord = {
1: 'ここはTOPページです。',
2: 'ここはAboutページです。',
};
/// 引数にidを渡し、該当するMessageを返すProvider
final messageProviderFamily = ProviderFamily<Message, int>(((ref, messageId) {
return Message(message: messageRecord[messageId]!, id: messageId);
}));
仮に、TopPage, AboutPageがあり、複数のWidgetがネストしているとする。
今回はそれぞれのPageがContainerA => ContainerB => TextWidgetというネストしたWidgetを子に持っており、TextWidgetはPageごとに異なるMessageを表示する必要がある。
class TopPage extends ConsumerWidget {
const TopPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('TOP Page'),
),
body: const Center(child: ContainerA(messageId: 1)),
);
}
}
class AboutPage extends ConsumerWidget {
const AboutPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('About Page'),
),
body: const Center(child: ContainerA(messageId: 2)));
}
}
class ContainerA extends ConsumerWidget {
const ContainerA({super.key, required this.messageId});
final int messageId;
Widget build(BuildContext context, WidgetRef ref) {
return ContainerB(messageId: messageId);
}
}
class ContainerB extends ConsumerWidget {
const ContainerB({super.key, required this.messageId});
final int messageId;
Widget build(BuildContext context, WidgetRef ref) {
return Center(child: TextWidget(messageId: messageId));
}
}
class TextWidget extends ConsumerWidget {
const TextWidget({super.key, required this.messageId});
final int messageId;
Widget build(BuildContext context, WidgetRef ref) {
/// ここでようやくバケツリレーしてきたidを渡し、messageを取得する
final message = ref.watch(messageProviderFamily(messageId)).message;
return Text(message);
}
}
messageIdをずっと渡し続けなければならなくて、非常に辛い。
また、Listで生成された複数の親が別々の子要素を持つみたいなケースでも同様の問題が発生する。
別に上の方法でもできるっちゃあできるものの、可読性が落ちるのでやりたくない。
Good(overrideさせる)
まずはOverrideさせる用にMessageのみを返すProviderを作る。
class Message {
const Message({this.message = 'No Massage', this.id = 1});
final String message;
final int id;
}
/// 仮でDBみたいなもの
Map<int, String> messageRecord = {
1: 'ここはTOPページです。',
2: 'ここはAboutページです。',
};
/// 引数にidを渡し、該当するMessageを返すProvider
final messageProviderFamily = ProviderFamily<Message, int>(((ref, messageId) {
return Message(message: messageRecord[messageId]!, id: messageId);
}));
+ /// OverrideするためのMessageのみを返すProviderを作る
+ final messageProvider = Provider<Message>(((ref) => const Message()));
次に、各PageでOverrideさせ、messageIdを渡す。
class TopPage extends ConsumerWidget {
const TopPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('TOP Page'),
),
- body: const Center(child: ContainerA(messageId: 1)),
+ body: const Center(child: ProviderScope(
+ overrides: [
+ messageProvider.overrideWithProvider(Provider(
+ ((ref) => ref.read(
+ messageProviderFamily(1),
+ )),
+ )),
+ ],
+ child: const ContainerA(),
+ ),
+ ),
+ );
}
}
class AboutPage extends ConsumerWidget {
const AboutPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('About Page'),
),
- body: const Center(child: ContainerA(messageId: 2)),
+ body: const Center(child: ProviderScope(
+ overrides: [
+ messageProvider.overrideWithProvider(Provider(
+ ((ref) => ref.read(
+ messageProviderFamily(2),
+ )),
+ )),
+ ],
+ child: const ContainerA(),
+ ),
+ ),
+ );
}
}
class ContainerA extends ConsumerWidget {
- const ContainerA({super.key, required this.messageId});
- final int messageId;
+ const ContainerA({super.key});
Widget build(BuildContext context, WidgetRef ref) {
- return ContainerB(messageId: messageId);
+ return ContainerB();
}
}
class ContainerB extends ConsumerWidget {
- const ContainerB({super.key, required this.messageId});
- final int messageId;
+ const ContainerB({super.key});
Widget build(BuildContext context, WidgetRef ref) {
- return Center(child: TextWidget(messageId: messageId));
+ return Center(child: TextWidget());
}
}
class TextWidget extends ConsumerWidget {
- const TextWidget({super.key, required this.messageId});
- final int messageId;
+ const TextWidget({super.key});
Widget build(BuildContext context, WidgetRef ref) {
- final message = ref.watch(messageProviderFamily(messageId)).message;
+ final message = ref.watch(messageProvider).message;
return Text(message);
}
}
バケツリレーしていた各Widgetの引数が消え、親でoverrideするだけで良くなりシンプルになった。
overrideって公式だとTestingでしか使われていないように見えたので、こういう用途もあって驚いた。
参考
Discussion