【Flutter】Riverpodを使っているウィジェットをWidgetbookでカタログ化する

FlutterでRiverpodを使っているウィジェットをWidgetbookでカタログ化する方法です。

flutter_riverpod 2.5.1
widgetbook 3.7.1

はじめに

本記事ではRiverpodを使っているウィジェットをWidgetbookでカタログ化する方法についてまとめます。

Widgetbook を使うとUIのカタログを作ることができますが、Riverpod を使うとUIが状態(やロジック)を参照することになります。
本記事では、Riverpod のプロバイダーをモッキングすることでWidgetbookを使ってUIをカタログ化する方法について紹介します。

なお、WidgetbookやRiverpodの基本的な使い方については以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

light11.hatenadiary.com

Riverpodを使ったウィジェットを作る

まず実行確認のため、Riverpodを使って簡単なアプリケーションを作ります。
まず以下の通り、int型の状態を保持してそれを増加させるメソッドを持つだけの簡単なプロバイダーを作ります。

// count.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'count.g.dart';

@riverpod
class Count extends _$Count {
  int _multiplier;

    // multiplierで値を設定すると、addする値にこの値が乗算される(モッキングの説明で使用します)
  set multiplier(int value) {
    _multiplier = value;
  }

  Count() : _multiplier = 1;

  @override
  int build(int initialValue) {
    return initialValue * _multiplier;
  }

  // 値を増加
  void add(int value) {
    state = state + value * _multiplier;
  }
}

コード生成機能を使っているので以下のコマンドで生成します。

$ dart run build_runner build

次にこれを使ったウィジェットを作ります。

// home.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'count.dart';

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(countProvider(123));
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$count'),
        TextButton(
          onPressed: () => ref.read(countProvider(123).notifier).add(1),
          child: const Text("Increment"),
        )
      ],
    );
  }
}

動作確認のためにこれを使ったアプリケーションも作っておきます。

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'home.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: Home());
  }
}

実行結果は下図のとおりです。

実行結果

appBuilderを使ってProviderScopeで囲う

Riverpod を使うためには、対象のウィジェットProviderScope で囲む必要があります。
Widgetbook のルートウィジェットのファクトリ(Widgetbook.material()など)にはappBuilder という引数があり、これを使うことで全てのウィジェットを任意のウィジェットの子孫にすることができます。
これを使って以下のようにProviderScopeで囲むことでRiverpodによるDIを可能にします。

// widgetbook.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
import 'widgetbook.directories.g.dart';

void main() {
  runApp(const WidgetbookApp());
}

@widgetbook.App()
class WidgetbookApp extends StatelessWidget {
  const WidgetbookApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Widgetbook.material(
      appBuilder: (context, child) {
        return ProviderScope(
          // RiverpodのProviderScopeで囲む
          child: MaterialApp(
            home: child,
          ),
        );
      },
      addons: [AlignmentAddon()],
      directories: directories,
    );
  }
}

これを実行すると、下図の結果が得られます。

結果

Providerを使ったウィジェットがWidgetbookに表示できました。

ProviderScopeのoverrideにプロバイダーのモックを設定

さて、実際には Provider は通信を行ったりしますが、Widgetbook はUIのカタログなので、Widgetbook に表示するときにはこのような処理はするべきではありません。
このような場合には、Provider のモックを作り動作をオーバーライドする必要があります。

モックの作り方は Riverpod におけるウィジェットテストと同様で、以下の記事にまとめていますので必要に応じて参照してください。

light11.hatenadiary.com

この方針に基づいて前節のクラスを修正したものが以下です。

// widgetbook.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
import 'count.dart';
import 'widgetbook.directories.g.dart';

void main() {
  runApp(const WidgetbookApp());
}

@widgetbook.App()
class WidgetbookApp extends StatelessWidget {
  const WidgetbookApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Widgetbook.material(
      appBuilder: (context, child) {
        // ProviderのOverrideを作成
        // 今回はテスト用に値が2倍になるProviderを作成
        var override =
            countProvider(123).overrideWith(() => Count()..multiplier = 2);
        return ProviderScope(
          // overridesに設定
          overrides: [
            override,
          ],
          child: MaterialApp(
            home: child,
          ),
        );
      },
      addons: [AlignmentAddon()],
      directories: directories,
    );
  }
}

今回はテスト用に値を2倍にするようにプロバイダーをオーバーライドしました。
実際には、通信する代わりに通信をシミュレートしたJsonを返すリポジトリを持つプロバイダーにオーバーライドするなど、適宜必要な処理をします。

これを実行すると以下の結果が得られます。

結果

正常にプロバイダーをオーバーライドできていることを確認できました。

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com