👻

[Flutter] Riverpodの.familyで引数を親ごとに定義する方法

2022/07/05に公開

Vueで言うところのProvide、Reactで言うところのRecoilみたく、別々の親から呼び出された共通のComponentに固有の状態をそれぞれ渡す必要があった。

.familyを使えば引数にユニークな値を与えてやればよいが、ネストが深いと親から子まで.family用のkeyをひたすら渡し続けなければならなくなる。

ショートアンサー

Providerを追加して、親のbuild内でProviderScope(override をする。

provider.dart
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をバケツリレーする。

provider.dart
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を表示する必要がある。

page.dart
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を作る。

provider.dart
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を渡す。

page.dart
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でしか使われていないように見えたので、こういう用途もあって驚いた。

参考

https://www.kamo-it.org/blog/riverpod-provider-scope/
https://twitter.com/remi_rousselet/status/1278545540490412032

Discussion