📗

[Widgetbook]Flutter版Storybookで社内デザインパッケージを運用する

2024/03/03に公開

はじめに

Flutter版のStorybookであるWidgetbookをWebで社内誰でも見れるようにしたので、その記録を記事にしました。

https://pub.dev/packages/widgetbook

Widgetbook とは

ドイツのスタートアップが作っているFlutter向けに開発されたカスタムUIカタログライブラリです。
Storybookを知っている人ならイメージがつきやすいと思いますし、実際にWidgetbook公式もStorybook.jsにインスパイアされていると記載されています。

Discover Widgetbook, a powerful Flutter package inspired by Storybook.js that simplifies the process of cataloging widgets, testing them across various devices and themes, and sharing them effortlessly with designers and clients.

実際に動くものを見た方が早いと思うので、公式が出しているWidgetbookを触ってみてください。

https://demo.widgetbook.io/#/?path=core/appbar/default

Widgetbook 導入の背景

所属している会社ではデザイナーがFigma上に親コンポーネントとなるマスターデータを作ってくれています。
イメージとしてはマテリアルデザインのFigmaみたいな感じです。


https://www.figma.com/file/o1KbN9ssV8CS6kssIRQ5ne/Material-3-Design-Kit-(Community)?type=design&node-id=47909-2&mode=design&t=gBirTvoZL1bkoVlW-11

Figmaのコンポーネントは使い回す前提で作られているため、実装も同じ粒度でWidgetを実装していきます。
しかし、コードだけではコンポーネントを探しづらいですし、下手したら同じWidgetを2つ作ってしまう可能性があります。
この課題はメンバーが増えるとより深刻化していくと思います。

そこで実装済みのWidgetをUIカタログとして見ることで開発効率をあげたいと考えました。

Widgetbook の導入方法

導入方法について軽く触れたいと思います。
スニダンでは /snkrdunk_designというディレクトリを切って、UIコンポーネント用のマルチパッケージを作っています。
/snkrdunk_design--template=pluginとして作られているためflutter build webはできません。
そのため、/snkrdunk_design/widgetbookというWidgetbook用の新しいFlutterプロジェクトを作成しています。

みやすくするとこんな感じです

.
├── lib
├── pubspec.yml
└── snkrdunk_design # 共通コンポーネント用のplugin
    ├── lib
    ├── pubspec.yml
    ├── test
    └── widgetbook # Widgetbook用のFlutterプロジェクト
        ├── lib
        │   ├── main.dart
        │   ├── main.directories.dart
        │   └── src
        │       └── button.dart
        └── pubspec.yml

こうすることで、それぞれのディレクトリ(パッケージ)は必要なだけのパッケージをpubspec.ymlに書けるので依存関係がはっきりしていて綺麗だと思います。
また、後述するWidgetbookのホスティングの際も/snkrdunk_designに変更がある時のみ行えば良いので効率が良いです。

pubspec.yml

packageは3つ入れます。本来はwidgetbookだけでも開発可能ですが、開発効率のためにwidgetbook_annotationwidgetbook_generatorを導入します。

dependencies:
  widgetbook: ^3.7.1
  widgetbook_annotation: ^3.1.0

  snkrdunk_design:
    path: ../

dev_dependencies:
  widgetbook_generator: ^3.7.0
  build_runner:

最新バージョンはこちらから

snkrdunk_design/widgetbook/main.dart

次にWidgetbookのvoid main()関数を記述するmain.dartの中身です。
見やすいために、後述するaddonsを空にしていますがとりあえず動きます。

lib/widgetbook.dart
import 'package:flutter/material.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());
}

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

  
  Widget build(BuildContext context) {
    return Widgetbook.material(
      // Use the generated directories variable
      directories: directories,
      addons: [],
      integrations: [
        // To make addons & knobs work with Widgetbook Cloud
        WidgetbookCloudIntegration(),
      ],
    );
  }
}

また、UIカタログとして表示したいコンポーネントは以下のように記述します。
今回はFilledButtonの例です。

lib/component/button.dart
import 'package:flutter/material.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

.UseCase(
    name: 'FilledButton',
    type: FilledButton,
Widget filledButtonUseCase(BuildContext context) {
  return Container(
    padding: const EdgeInsets.only(top: 100),
    width: double.infinity,
    alignment: Alignment.center,
    child: Column(
      children: [
        FilledButton(
          onPressed: () {},
          child: const Text("Enabled"),
        ),
        const SizedBox(height: 16),
        const FilledButton(
          onPressed: null,
          child: Text('Disabled'),
        ),
      ],
    ),
  );
}

最後にbuild_runnerを回しましょう!

$ flutter pub run build_runner build --delete-conflicting-outputs

flutter run -d chrome -t 該当のmain.dartこれで以下のような画面が見れるはずです。

それぞれの詳しい説明などは公式が分かりやすいので是非読んでください。
https://docs.widgetbook.io/getting-started/install

Widgetbook を活用する(Addon)

このままでも便利ですが、Addonという機能を使ってより便利にすることができます。
Addonとは画面右側のサイドバーに出ている部分のことで、ダークモードを表示したり、テキストサイズを2倍に変更したりなどができます。

おすすめのAddonsはここら辺です

  • DeviceFrameAddon
  • TextScaleAddon
  • MaterialThemeAddon
  • InspectorAddon

Widgetbook をみんなに見てもらう

Widgetbookを閲覧したい時に、都度flutter runしていては開発効率は上がりませんし、Flutterエンジニア以外が見ることができません。
開発者、または社内の開発者以外の人が簡単に閲覧できるために、今回はホスティング環境を3つ検討しました。

Widgetbook cloud

公式が出しているWidgetbook cloudを使えば簡単にホスティングができそうです。
しかし、現在招待制のため申請したのですがwaiting listに登録したよと言われてから2ヶ月が経ちそうです。

GitHub pages

Flutter WebもWebのため、GitHub pagesが使えます。ただし閲覧権限等でGitHubはめんどそうだなぁーと思いやめました。
publicにしている個人開発のリポジトリなどは相性良さそうです。
実際にこちらのプロジェクトでGitHub pagesにデプロイするGitHub Actionsを組んでみました。

https://github.com/imajoriri/widgetbook_sample/blob/main/.github/workflows/deploy.yml

AWS s3 ←これにした

ほかに考えられるのはs3などの外部のホスティング環境ですが、ありがたいことに社内では誰でも使えるs3環境がありました。
閲覧に関してもVPNを繋いでいたら誰でも閲覧可能な環境です。
そのため、今回はs3を採用しました。

s3もGitHub Actionsでデプロイ可能で、以下のようなActionを組んでいます。

トリガーはdevelopブランチにマージされた時で、"snkrdunk_design/**"に変化があった時のみ走らせています。
また、build時に--base-hrefオプションでパスを指定しないとs3で見ることができません。

name: deploy widgetbook for s3 only develop branch and widgetbook directory changes

on:
  push:
    branches:
      - develop
    paths:
      - "snkrdunk_design/**"

jobs:
  deploy_widgetbook:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v3.3.0

      - name: Install dependencies
        uses: ./.github/actions/install_dependencies

      - uses: aws-actions/configure-aws-credentials@v4.0.1
        with:
          role-to-assume: arn:aws:iam::xxxxxxxxxx:role/xxxx
          aws-region: ap-northeast-1
          role-duration-seconds: 3600
          role-session-name: xxx

      # --base-hrefには、s3のバケット名を指定する
      - name: build web
        run: |
          cd snkrdunk_design/widgetbook
          flutter build web --base-href=/xxx/xxx/

      - name: Copy files to s3
        run: |
          cd snkrdunk_design/widgetbook
          aws s3 cp ./build/web/ s3://xxx/xxx/xxx/ --acl public-read --recursive

Widgetbook 発展のために

最後に、まだ取り組めていないですが、今後Widgetbookを使ってやりたいことを書きます。

Knobs

UIカタログ内のテキストや数値、カラーを右サイドバーから変更できる機能です。
ボタンのテキストなどは可変なことが多く、その時々で文字を設定して検証したいケースも多いかと思いますが、そういう時に役立ちそうです。

Goldenテストに再利用できるのでは?

スニダンでは最近Goldenテストを取り入れ始めています。
例えば先ほど紹介したこちらのコードですが、filledButtonUseCaseの部分をGoldenテストに再利用できるような気がしました。
しかし、Knobを使っている場合エラーになってしまうようです。

そんな中公式のリポジトリを漁っていると、widgetbook_4というリポジトリでゴールデンテストの再利用をしやすくするためについて書かれていました。
詳細は省きますが、将来取り入れられる可能性が高そうです。

https://github.com/widgetbook/widgetbook_4?tab=readme-ov-file#golden-tests-structure

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

.UseCase(
    name: 'FilledButton',
    type: FilledButton,
Widget filledButtonUseCase(BuildContext context) {
  return Container(
    padding: const EdgeInsets.only(top: 100),
    width: double.infinity,
    alignment: Alignment.center,
    child: Column(
      children: [
        FilledButton(
          onPressed: () {},
          child: const Text("Enabled"),
        ),
        const SizedBox(height: 16),
        const FilledButton(
          onPressed: null,
          child: Text('Disabled'),
        ),
      ],
    ),
  );
}

さいごに

最後まで読んでいただきありがとうございます。
仕事でやってること、個人開発でやってることなど色々発信していこうと思うので良かったらフォローしてください!

Discussion