WidgetbookとRiverpodを組み合わせて利用するときのTips

2023/07/18に公開

WidgetbookとRiverpodを組み合わせて利用するときのTipsです。

  1. overrides
  2. knobコードを記述する場所
  3. Loadingの表示

1. overrides

UseCaseでFamilyをオーバーライドするときに、そのFamilyの引数が変更されるとエラーが発生します。 エラーはknobの値を変更する時と、複数のUseCaseで同じFamilyをオーバーライドしている時に発生します。

以下のコードを考えます。


Future<String> message(MessageRef ref, {required String name}) async {
  return 'Hi $name';
}
.UseCase(
  name: 'Data',
  type: MessageScreen,
)
Widget messageScreenData(BuildContext context) {
  final name = context.knobs.string(label: 'name', initialValue: 'Taro');
  return ProviderScope(
    overrides: [
      messageProvider(name: name).overrideWith((ref) => 'Message to $name'),
    ],
    child: MessageScreen(name: name),
  );
}

このコードではFamilyのオーバーライドを行い、またknobを利用しています。

Widgetbookアプリでknobを書き換える時に以下のエラーが発生します。

The following assertion was thrown building ProviderScope(state: ProviderScopeState#9f721):
Replaced the override of type Null with an override of type AutoDisposeFutureProvider<String>, which is different.
Changing the kind of override or reordering overrides is not supported.

これはmessageProvider(name:)の引数が変更され、オーバーライドの変更が起こるためです。

また複数のUseCaseで同じFamilyをオーバーライドしている場合でもエラーが発生します。

この問題はUseCaseでオーバーライドを行うときに常につきまとう問題です。
私はこれを回避するためにWidgetbookIntegrationを利用して、オーバーライドしたProviderContainerをWidgetbookの更新ごとに作成して利用しています。

まず以下のようなWidgetbookIntegrationを作成し、WidgetbookStateからそのIntegrationを取得できるようにします。

class RiverpodIntegration extends WidgetbookIntegration {
  ProviderContainer get container {
    final overrides = _overrides;
    _overrides = [];    // overridesを書き換えない時に意図しないoverridesによる挙動を防ぐため初期化しておく
    return ProviderContainer(overrides: overrides);
  }

  List<Override> _overrides = [];
  set overrides(List<Override> value) => _overrides = value;
}

extension RiverpodIntegrationExtension on WidgetbookState {
  RiverpodIntegration get riverpodIntegration =>
      integrations?.firstWhere((element) => element is RiverpodIntegration) as RiverpodIntegration;
}

次にこのIntegrationを利用してWidgetbookAppでオーバーライドを行います。

.App()
class WidgetbookApp extends StatelessWidget {
  const WidgetbookApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Widgetbook.material(
      directories: directories,
      appBuilder: (context, child) => UncontrolledProviderScope(
        container: WidgetbookState.of(context).riverpodIntegration.container,
        child: child,
      ),
      integrations: [
        RiverpodIntegration(),
      ],
    );
  }
}

UseCaseは以下のように記述します。

.UseCase(
  name: 'Data',
  type: MessageScreen,
)
Widget messageScreenData(BuildContext context) {
  final name = context.knobs.string(label: 'name', initialValue: 'Taro');
  WidgetbookState.of(context).riverpodIntegration.overrides = [
    messageProvider(name: name).overrideWith((ref) => 'Message to $name'),
  ];
  return MessageScreen(key: UniqueKey(), name: name);
}

UseCaseで生成するWidgetのkeyにUniqueKeyを指定することで、Widgetbookの更新ごとにWidgetが再生成されるようにします。

これによりknobでUseCaseが再読み込みされる時にProviderContainerが新しくなり、エラーが発生しなくなります。

2. knobコードを記述する場所

上記のコードではknobコードをoverrideWithメソッドの外で記述しています。nameをWidgetに渡すためなのですが、もしWidgetに渡す必要がない場合にknobのメソッドをoverrideWithメソッドの高階関数の中で利用すると、knobのメソッドが実行されるタイミングが遅くなり、knobが効果を発揮しなくなります。

ですのでknobメソッドはUseCaseメソッドが実行される中で記述する必要があります。

3. Loadingの表示

WidgetbookでLoadingにしたままにするには以下のように記述し、Completerのfutureを返すことでLoadingのままになります。

.UseCase(
  name: 'Loading',
  type: MessageScreen,
)
Widget messageScreenLoading(BuildContext context) {
  WidgetbookState.of(context).riverpodIntegration.overrides = [
    messageProvider(name: 'Taro').overrideWith((ref) => Completer<String>().future)
  ];
  return const MessageScreen(name: 'Taro');
}

まとめ

WidgetbookとRiverpodを組み合わせて利用するときのTipsを紹介しました。役立てていただけると嬉しいです。

またIntegrationを利用する方法より良い方法があれば教えてください。

株式会社ゆめみ

Discussion