WidgetbookとRiverpodを組み合わせて利用するときのTips
WidgetbookとRiverpodを組み合わせて利用するときのTipsです。
- overrides
- knobコードを記述する場所
- 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