📚

【Flutter】Storybookライクに使えるMonarchを試してみる

2022/08/11に公開

Build high-quality UIs with ease | Monarch

MonarchはFlutterでStorybookみたく、Widgetの確認やデバッグ、テストを行う為のツールで、
色々充実してそうなので、試してみたいと思います。

試した環境

$ sw_vers
ProductName:	macOS
ProductVersion:	12.4
BuildVersion:	21F79

テスト用のプロジェクト作成

$ mkdir monarch_sample
$ cd monarch_sample
$ fvm use 3.0.5 --force
$ fvm flutter create --org com.sample.monarch .

インストール

こちらを参考にインストールしていきます。
※ macosの場合は、事前にXcodeが必要です。

$ curl -O https://d2dpq905ksf9xw.cloudfront.net/macos/monarch_macos_1.7.8.zip
$ unzip monarch_macos_1.7.8.zip

monarch/bin にパスを通して、monarch コマンドが使えるようにしておく。

初期化して起動

先程の monarch_sample プロジェクト直下に移動し、以下コマンドを実施します。

$ monarch init

↑を実施すると pubspec.yaml には以下が追加されていました。

dev_dependencies:
  monarch: ^2.3.0
  build_runner: ^2.1.0

また、以下の build.yaml とサンプルの stories ディレクトリが作成されていました。

targets:
  $default:
    sources:
      - $package$
      - lib/**
      - stories/**

サンプルとして作成された、stories/sample_button.dartstories/sample_button_stories.dart は以下の内容で作成されていました。

ソースコードはこちら
stories/sample_button.dart
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

enum ButtonStyles { primary, secondary, disabled }

class Button extends StatelessWidget {
  final String text;
  final ButtonStyles style;

  Button(this.text, this.style);
  
  Widget build(BuildContext context) {
    return Center(
        child: TextButton(
            onPressed: () => null,
            style: TextButton.styleFrom(
                primary: getPrimaryColor(),
                backgroundColor: getBackgroundColor(),
                side: style == ButtonStyles.secondary
                    ? BorderSide(width: 0, color: Colors.black87)
                    : null),
            child: Text(this.text)));
  }

  Color getPrimaryColor() {
    switch (style) {
      case ButtonStyles.primary:
        return Colors.white;
      case ButtonStyles.secondary:
        return Colors.black87;
      case ButtonStyles.disabled:
        return Colors.white;
      default:
        return Colors.white;
    }
  }

  Color getBackgroundColor() {
    switch (style) {
      case ButtonStyles.primary:
        return Colors.green;
      case ButtonStyles.secondary:
        return Colors.white;
      case ButtonStyles.disabled:
        return Color(0xFFE0E0E0);
      default:
        return Colors.green;
    }
  }
}
stories/sample_button_stories.dart
import 'package:flutter/material.dart';
import 'sample_button.dart';

Widget primary() => Button(
  'Button', ButtonStyles.primary);

Widget secondary() => Button(
  'Button', ButtonStyles.secondary);

Widget disabled() => Button(
  'Button', ButtonStyles.disabled);

早速、起動してみます。

$ monarch run 
Using flutter sdk at /Users/xxxx/fvm/versions/3.0.5/bin/flutter
Enabling Flutter for desktop

Downloading the Monarch UI for this project's flutter version...

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 31.4M  100 31.4M    0     0  6578k      0  0:00:04  0:00:04 --:--:-- 6750k

Extracting Monarch UI zip... Done.

Starting Monarch.

Preparing stories...
Preparing stories completed, took 47.6sec.

Launching Monarch app...
Launching Monarch app completed, took 2.6sec.

Attaching to stories...
Attaching to stories completed, took 16.5sec.

Setting up stories watch...
3.1sec elapsed
Setting up stories watch completed, took 4.5sec.

Monarch key commands:
r Hot reload (default).
R Hot restart.
h Show this list of commands.
⌃C Quit.

Monarch is ready. Project changes will reload automatically with hot reload.
Press "R" to force a hot restart.

fvm を使用していたら fvm のflutter sdkを使用するようです。また、flutter sdk の desktop を有効化しています。
実行すると専用のデスクトップアプリが起動し、Storiesを選択したり挙動やデバイスの切り替え等直感的に操作できる感じです ✨

ちなみに↑のデスクトップアプリですが、
monarch/bin/cache/monarch_ui/flutter_macos_3.0.5-stable 配下にインストールされているようでした。

試しに何かStoriesを書いてみる

よくありそうな、Iconの上に何かの件数を表すbadgeを表示する事ができる、BadgeIcon Widgetを作成しそのStoriesを書いてみたいと思います。
↓まずは BadgeIcon の実装になります。(細かいリファクタ箇所はありそうですが、今回仮で、、)

lib/widgets/badge_icon.dart
import 'package:flutter/material.dart';

class BadgeIcon extends StatelessWidget {
  const BadgeIcon({Key? key, required this.icon, this.badgeCount = 0})
      : super(key: key);

  final Widget icon;
  final int badgeCount;

  
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[icon, _badge()],
    );
  }

  Widget _badge() {
    return Positioned(
        top: 0,
        right: 0,
        child: Container(
          padding: const EdgeInsets.all(1),
          constraints: const BoxConstraints(minHeight: 13, minWidth: 13),
          decoration: BoxDecoration(
              color: Colors.red, borderRadius: BorderRadius.circular(7)),
          child: Text(
            badgeCount.toString(),
            style: const TextStyle(fontSize: 8, color: Colors.white),
            textAlign: TextAlign.center,
          ),
        ));
  }
}

まずは、0100 をバッジ表示する BadgeIcon の Storiesを書いてみます。

lib/widgets/badge_icon_stories.dart
import 'package:flutter/material.dart';
import 'badge_icon.dart';

Widget countZero() => const BadgeIcon(icon: Icon(Icons.mail));

Widget countOneHundred() => const BadgeIcon(
      icon: Icon(Icons.mail),
      badgeCount: 100,
    );

早速起動してみると、Storiesが増えていて BadgeIcon 関連の見た目を確認できます。

Riverpodと一緒に使う場合

いいサンプルかは分かりませんが、、先程の BadgeIcon とRiverpodを使うWidgetを作成してみたいと思います。

まずは、flutter_riverpod をインストールします。

dependencies:
  flutter_riverpod: ^1.0.4

新規に BadgeIconWithProvider というWidgetを以下内容で作成します。

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

final countProvider = StateProvider.autoDispose<int>((ref) => 0);

class BadgeIconWithProvider extends ConsumerWidget {
  const BadgeIconWithProvider({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(countProvider);
    return Container(
      padding: const EdgeInsets.all(8),
      child: BadgeIcon(
        icon: const Icon(Icons.mail),
        badgeCount: count,
      ),
    );
  }
}

やっている事は countProvider から渡ってくる値を BadgeIcon のカウントとして渡しているだけのWidgetになります。
次にStoriesを書いてみます。

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

class CounterBody extends ConsumerWidget {
  const CounterBody({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final notifier = ref.watch(countProvider.notifier);
    return Column(
      children: <Widget>[
        const BadgeIconWithProvider(),
        const SizedBox(height: 5),
        ElevatedButton(
          child: const Text('カウントアップ'),
          onPressed: () => notifier.update((state) => state + 1),
        ),
        const SizedBox(height: 5),
        ElevatedButton(
          child: const Text('カウントダウン'),
          onPressed: () => notifier.update((state) => state - 1),
        ),
      ],
    );
  }
}

Widget counter() => const ProviderScope(child: CounterBody());

本家のStorybookだとパラメータを入力してコンポーネントを確認できたりすると思うのですが、
Monarchの場合そういった事ができなさそうなので、カウントアップ、カウントダウンするボタンを設けました。
ボタン押下で StateProvider の値を変化させています。

まとめ

StorybookみたくWidget単体でフォーカスして作っていけそうなので、今後も使っていこうと思います!
記事内には書いてないですが、カスタムテーマの切り替えや複数Locale切り替えも対応しているので、ここら辺もまた使って行きたいと思います。

Discussion