Zenn

【Widgetbook】UIコンポーネントから開発しよう(Flutter,ハンズオン)

2024/12/22に公開

WidgetbookでUIコンポーネントから開発しよう

WidgetbookでUIコンポーネントから開発しよう

Widgetbookとは

Flutterを用いたアプリケーション開発において、UIコンポーネントの管理は非常に重要です。
Widgetbookは、FlutterのWidgetをカタログのように一覧表示し、個々のWidgetの動作を簡単に確認できるライブラリです。
シミュレーター上でWidgetを操作しているかのように、ブラウザ上でさまざまな状態のWidgetを視覚的に確認できます。

Widgetbookを使うことで、以下のような事が可能になります。

  • ビジネスロジックと分離してUIコンポーネントの開発を進める
  • 異なるデバイスのレイアウトや、ダークモード/ライトモードの表示などを確認
  • Widgetbookの操作画面をデプロイしデザイナーとフィードバックを共有

この記事では、Widgetbookプロジェクトの用意とユースケースの追加、アドオンの活用、ノブの活用について解説します。
ぜひ、FlutterのUIコンポーネント開発に役立ててください👇

  1. サンプルプロジェクトの紹介
  2. Widgetbookプロジェクトの用意
  3. ユースケースの追加
  4. 開発環境の整備
  5. アドオンの活用
  6. ノブの活用
  7. Widgetbook活用方法の考察

サンプルプロジェクトの紹介

この記事で紹介している手順はサンプルプロジェクトで実施したものをもとに作成しています。
そのサンプルプロジェクトをGitHubに公開しています。

https://github.com/hott3/widgetbook_trial

Widgetbookプロジェクトの用意

任意のディレクトリにプロジェクトを作成

ここで作成したプロジェクトがアプリプロジェクトとなります。

terminal
flutter create widgetbook_trial --empty

Widgetbookプロジェクトをサブプロジェクトとして作成

アプリプロジェクトを作成後、Widgetbookプロジェクトをサブプロジェクトとして作成します。

terminal
cd widgetbook_trial
flutter create widgetbook --empty --platforms web

Widgetbookプロジェクトのpubspec.yamlを編集

Widgetbookパッケージと名前衝突を避けるために、作成したWidgetbookプロジェクトのpubspec.yamlを編集します。

widgetbook/pubspec.yaml
- name: widgetbook
+ name: widgetbook_workspace

Widgetbookプロジェクトで使用するパッケージを追加

Widgetbookプロジェクトで使用するパッケージを追加します。

terminal
cd widgetbook
flutter pub add widgetbook widgetbook_annotation dev:widgetbook_generator dev:build_runner

追加したパッケージは以下の通りです。

Widgetbookプロジェクトのパス依存関係にアプリプロジェクトを追加

Widgetbookプロジェクトのパス依存関係に、アプリプロジェクトを追加します。
依存関係を追加することで、Widgetbookプロジェクトの開発時にアプリプロジェクトのコードを参照できるようになります。
アプリプロジェクトのコードを複製せずにimport文を追加することができるので二重管理の手間は起こりません。

widgetbook/pubspec.yaml
dependencies:
+  widgetbook_trial:
+    path: ../

ここでフォルダ構造を確認する[1]と以下のようになります。
widgetbook_trialがアプリプロジェクトを開発するフォルダ、widgetbookフォルダがWidgetbookプロジェクトを開発するフォルダです。

terminal
widgetbook_trial $ tree -L 2
.
├── README.md
├── analysis_options.yaml
├── lib
│   └── main.dart
├── pubspec.lock
├── pubspec.yaml
├── web
│   ├── favicon.png
│   ├── icons
│   ├── index.html
│   └── manifest.json
├── widgetbook
│   ├── README.md
│   ├── analysis_options.yaml
│   ├── lib
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   ├── web
│   └── widgetbook.iml
└── widgetbook_trial.iml

7 directories, 14 files

ユースケースの追加

次にWidgetbookプロジェクトでUIコンポーネントを確認できるようにアプリプロジェクトとWidgetbookプロジェクトにファイルを追加します。

アプリプロジェクトにUIコンポーネントを追加

アプリプロジェクトにthanks_button.dartを追加します。

widgetbook_trial/lib/thanks_button.dart
import 'package:flutter/material.dart';

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

  
  Widget build(BuildContext context) {
    return OutlinedButton.icon(
      onPressed: () {
        showDialog<String>(
            context: context,
            builder: (BuildContext context) => Dialog(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Text(
                      'どういたしまして!',
                      textAlign: TextAlign.center,
                    ),
                  ),
                ));
      },
      icon: Icon(Icons.favorite),
      label: Text('ありがとう!'),
    );
  }
}

Widgetbookプロジェクトにユースケースを追加

WidgetbookプロジェクトにThanksButtonを表示するためのユースケースを追加します。

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

.UseCase(name: 'Default', type: ThanksButton)
Widget buildThanksButtonUseCase(BuildContext context) {
  return ThanksButton();
}

Widgetbookプロジェクトのmain.dartを編集

Widgetbookプロジェクトのmain.dartを以下のように書き換えます。

widgetbook/lib/main.dart
import 'package:flutter/material.dart';
+import 'package:widgetbook/widgetbook.dart';
+import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
+
+import 'main.directories.g.dart';

void main() {
- runApp(const MainApp());
+ runApp(const WidgetbookApp());
}

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

  
  Widget build(BuildContext context) {
-   return const MaterialApp(
-     home: Scaffold(
-       body: Center(
-         child: Text('Hello World!'),
-       ),
-     ),
+   return Widgetbook.material(
+     directories: directories,
    );
  }
}

Widgetbookプロジェクトのmain.dartを書き換えると以下のようなファイルになります。

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

// まだ存在しないファイルですが、次のステップで生成されます
import 'main.directories.g.dart';

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

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

  
  Widget build(BuildContext context) {
    return Widgetbook.material(
      // まだ未定義ですが、次のステップで生成されます
      directories: directories,
    );
  }
}

Widgetbookプロジェクトでビルドランナーを実行

ビルドランナーを実行して、directoriesを含んだmain.directories.g.dartを生成します。

cd widgetbook
dart run build_runner build -d
ファイルの編集を監視して自動でビルドする場合
dart run build_runner watch -d

Widgetbookプロジェクトの実行

これでWidgetbookプロジェクトの準備が完了したので実行してみます。

flutter run -d chrome

Widgetbookが立ち上がった後に、画面左側のペインからThanksButtonをクリックします。
以下のようにThanksButtonが表示されれば成功です🎉🎉🎉

開発環境の整備

Widgetbookの実行が確認できたところで、この後の開発を効率的に行うために開発環境を整備します。

WidgetbookプロジェクトにmacOSプラットフォームを追加

FlutterをWebのプラットフォームで実行する場合、ホットリロードが機能しません。[2]
ホットリロードを有効にして開発するために、macOSのプロジェクトを追加します。※macOSユーザーのみ

cd widgetbook
flutter create --platforms macos .

VSCodeのデバッグ環境を設定

VSCodeを利用している場合、launch.jsonを追加することでデバッグ環境の設定と実行を効率的に行うことができます。
以下のファイルを.vscode/launch.jsonとして作成することで、さきほどの実行コマンドを叩く必要がなく、VSCodeの「実行とデバッグ」メニューからWidgetbookプロジェクトを実行できます。

.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "debug widgetbook",
            "request": "launch",
            "type": "dart",
            "program": "widgetbook/lib/main.dart",
            "flutterMode": "debug",
        },
    ]
}

アドオン

Widgetbookには確認画面の設定を動的に変更できるアドオンが用意されています。
Widgetbookで用意されているアドオンを使用することで異なる端末のレイアウト確認やテーマの切り替え、テキストのサイズ変更などが可能になります。

アドオンの追加方法

widgetbook/lib/main.dartを編集して、Widgetbookに用意されているいくつかのアドオンを追加してみます。

widgetbook/lib/main.dart
  
  Widget build(BuildContext context) {
    return Widgetbook.material(
      directories: directories,
+     addons: [
+       DeviceFrameAddon(
+         initialDevice: Devices.ios.iPhone13,
+         devices: [
+           Devices.ios.iPhone13,
+           Devices.ios.iPhone13ProMax,
+           Devices.android.mediumPhone,
+         ],
+       ),
+       MaterialThemeAddon(
+         themes: [
+           WidgetbookTheme(name: 'Light', data: ThemeData.light()),
+           WidgetbookTheme(name: 'Dark', data: ThemeData.dark()),
+         ],
+       ),
+       TextScaleAddon(
+         min: 0.5,
+         max: 3.0,
+         divisions: 5,
+       ),
+       BuilderAddon(
+         name: 'Scaffold & SafeArea',
+         builder: (context, child) {
+           return Scaffold(
+             body: SingleChildScrollView(
+               child: SizedBox(
+                 width: double.infinity,
+                 child: SafeArea(
+                   child: child,
+                 ),
+               ),
+             ),
+           );
+         },
+       ),
+     ],
    );
  }

DeviceFrameAddon

DeviceFrameAddonはデバイスの枠を切り替えるアドオンです。
ここで指定したデバイスの枠が表示されるのでプロジェクトに合わせて必要なデバイスの画面を用意できます。

MaterialThemeAddon

MaterialThemeAddonはテーマを切り替えるアドオンです。
最初から用意されているThemeDataだけでなく、ThemeDataを拡張することでオリジナルのテーマを追加することもできます。

TextScaleAddon

TextScaleAddonはテキストのサイズを切り替えるアドオンです。
端末の設定で変更されるテキストサイズのテストに利用することができるので、アクセシビリティ対応の確認に利用できます。

BuilderAddon

BuilderAddonはユースケースをラップするWidgetを指定できるアドオンです。
ここで指定したWidgetがユースケースをラップするので、ScaffoldSafeAreaなどの毎回呼び出すWidgetを指定しておくことができます。

その他のアドオン

他にも用意されているアドオンがあり、公式ドキュメントに詳細が記載されています。
言語設定のアドオンは、異なる言語のテキストが反映されたときにUIコンポーネントがどのように表示されるか確認するのに便利そうです。

Available Addons

Widgetbook comes equipped with a variety of ready-to-use Addons:

  • Theme Addon: Provides theming options.
    • Material Theme Addon
    • Cupertino Theme Addon
    • Custom Theme Addon
  • Device Frame Addon: Facilitates previewing use-cases on specific device sizes.
  • Localization Addon: Enables testing across different locales.
  • Text Scale Addon: Supports varying text scales and aids in accessibility testing.
  • Alignment Addon: Enables centering the use-case within the workbench.
  • Grid Addon: Enables displaying a pixel grid.

引用)Addons - Widgetbook

ノブ

Widgetbookにはユースケースに渡すパラメータをその場で変更できるノブが用意されています。
Widgetbookで用意されているノブを利用することで、さまざまな条件や入力に基づいてUIコンポーネントを調整できるので、UIコンポーネントの動作確認やデザイン検証が効率的に行えます。

ノブの追加方法

アプリプロジェクトのWidgetにプロパティを追加

まず、アプリプロジェクトのthanks_button.dartにプロパティを追加します。

lib/thanks_button.dart
class ThanksButton extends StatelessWidget {
- const ThanksButton({super.key});
+ const ThanksButton({
+   super.key,
+   required this.buttonLabel,
+   required this.responseText,
+   required this.color,
+   required this.textStyle,
+ });
+
+ final String buttonLabel;
+ final String responseText;
+ final Color color;
+ final TextStyle textStyle;

  
  Widget build(BuildContext context) {
    return OutlinedButton.icon(
      onPressed: () {
        showDialog<String>(
            context: context,
            builder: (BuildContext context) => Dialog(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Text(
-                     'どういたしまして!',
+                     responseText,
                      textAlign: TextAlign.center,
                    ),
                  ),
                ));
      },
-     icon: Icon(Icons.favorite),
-     label: Text('ありがとう!'),
+     icon: Icon(Icons.favorite, color: color),
+     label: Text(buttonLabel, style: textStyle.copyWith(color: color)),
    );
  }
}

Widgetbookプロジェクトのユースケースにノブを追加

Widgetbookプロジェクトのユースケースにノブを追加します。
追加したノブを使って、ThanksButtonのプロパティに指定します。

widgetbook/lib/thanks_button.dart
@widgetbook.UseCase(name: 'Default', type: ThanksButton)
Widget buildThanksButtonUseCase(BuildContext context) {
- return ThanksButton();
+ final buttonLabel = context.knobs.string(
+   label: 'Button Label',
+   initialValue: 'ありがとう!',
+ );
+ final responseText = context.knobs.string(
+   label: 'Response Text',
+   initialValue: 'どういたしまして!',
+ );
+ final color = context.knobs.color(
+   label: 'Color',
+   initialValue: ThemeData().colorScheme.primary,
+ );
+ final textStyleOption = [
+   _TextStyleOption(
+     optionLabel: 'Large',
+     textStyle: TextStyle(
+       fontSize: 14,
+     ),
+   ),
+   _TextStyleOption(
+     optionLabel: 'Medium',
+     textStyle: TextStyle(
+       fontSize: 12,
+     ),
+   ),
+   _TextStyleOption(
+     optionLabel: 'Small',
+     textStyle: TextStyle(
+       fontSize: 11,
+     ),
+   ),
+ ];
+ final textStyle = context.knobs.list(
+   label: 'TextStyle',
+   initialOption: textStyleOption[0],
+   labelBuilder: (option) => option.optionLabel,
+   options: textStyleOption,
+ );
+
+ return ThanksButton(
+   buttonLabel: buttonLabel,
+   responseText: responseText,
+   color: color,
+   textStyle: textStyle.textStyle,
+ );
+ }
+ 
+ class _TextStyleOption {
+   const _TextStyleOption({
+     required this.optionLabel,
+     required this.textStyle,
+   });
+ 
+   final String optionLabel;
+   final TextStyle textStyle;
}

String Knob

context.knobs.string()はテキストを変更するノブです。
Widgetbookの画面上でテキストを入力することで、ThanksButtonのプロパティを変更できます。

Color Knob

context.knobs.color()は色の値を変更するノブです。
Widgetbookの画面上で色を選択することで、ThanksButtonのプロパティを変更できます。

List Knob

context.knobs.list()はリストから値を選択するノブです。
リストの選択肢には任意の型を使用できます。
Widgetbookの画面上でリストから値を選択することで、ThanksButtonのプロパティを変更できます。

その他のノブ

他にも用意されているノブがあり、公式ドキュメントに詳細が記載されています。

Type Regular Knob Nullable Knob
bool boolean booleanOrNull
int int.input int.input
int int.slider int.slider
double double.input doubleOrNull.input
double double.slider doubleOrNull.slider
String string stringOrNull
Duration duration durationOrNull
DateTime dateTime dateTimeOrNull
Color color colorOrNull
T list listOrNull

引用)Knobs - Widgetbook

Widgetbook活用方法の考察

開発体制に与える影響

この記事ではエンジニアがUIコンポーネントを開発する際に、Widgetbookを利用する手順を解説しました。
開発したWidgetbookプロジェクトをデプロイしてGitHub PagesやFirebase Hostingにデプロイすることで、エンジニア以外のメンバーにもUIコンポーネントの確認を共有できます。
機能の仕様を調整しながらUIコンポーネントを先行して開発を進めるなど、柔軟な開発を進めることができるのもWidgetbookの特徴です。
もちろん機能の開発段階に進んだとき、UIコンポーネントの調整/修正を行う可能性もあります。
その際は、もとのアプリプロジェクトのコードを依存関係で連携しているので、アプリプロジェクトの作業はWidgetbookプロジェクトに反映させることができます。

ユースケースを追加する手間

UIコンポーネントはアプリプロジェクトの中で行われますが、Widgetbookプロジェクトにユースケースを追加する手間がかかります。
たとえば、既存のUIコンポーネントにプロパティが追加された場合、ユースケースを編集して追加されたプロパティに対応する必要があります。
たとえば、新規のUIコンポーネントを追加された場合、どのようなUIコンポーネントなのか利用イメージを想定してユースケースを追加する必要があります。
私はこれらの作業は反復的で手間を感じる作業に感じています。

調べていると、VSCodeにWidgetbook Entries Generatorという拡張機能があることを知りました。
この拡張機能はユースケースを追加するプロセスを自動化し、手動で設定する時間のかかる作業を効率化できるようです。

https://leancode.co/blog/moving-flutter-widgets-to-widgetbook

UIコンポーネントが少ない場合や重要なUIコンポーネントのみを管理する場合は、まずは手動でユースケースを追加しておき、その後に拡張機能の利用を検討するのも良いかもしれません。

参考記事など

参考になりました🙇‍♂️

https://zenn.dev/natoring/articles/3d6638ab499117
https://zenn.dev/imajoriri/articles/6b10a2351fe887
https://qiita.com/Ryota-Nakamura-317/items/b381230a890133dd611b
https://light11.hatenadiary.com/entry/2024/08/05/191249
https://kakehashi-dev.hatenablog.com/entry/2024/09/24/110000

まとめ

Widgetbookを利用してUIコンポーネントから開発する手順をまとめました。
UI/UXを重視したユーザー体験の向上にはUIコンポーネントの管理が重要です。
ビジネスロジックと分離してUIコンポーネントの開発が必要になったとき、試してみてはいかがでしょうか。
ぜひこの記事をご参考くださいませ💡
もし参考になったところや、疑問に感じたところがあればコメントください🌻

脚注
  1. treeコマンドにtree — Homebrew Formulaeを使用しています ↩︎

  2. Flutter Webはホットリスタートをサポートしていますが、ホットリロードはサポートしていません。参照 ↩︎

GitHubで編集を提案

Discussion

ログインするとコメントできます