LogoWidgetbook

Efficiently Using Widgetbook in a Monorepo Setting

In the diverse Flutter ecosystem, it's commonplace for projects to segregate their UI components into distinct packages. Although these UI component packages are pivotal, they seldom form a standalone desktop application. A common hurdle for developers arises when wanting to leverage Widgetbook for these packages.

This document shows a streamlined approach to seamlessly integrate Widgetbook within a monorepo structure. The doc considers a tool-agnostic approach where you can take any part of it and integrate it into your monorepo structure and use any tool that you are comfortable with.

A Primer on Monorepo Structure#

To commence, we present an elementary monorepo framework for a Flutter project which can serve as a foundation.

1. Unpacking the Monorepo Blueprint#

Here's an ideal monorepo configuration tailored for Widgetbook:

  • apps: Houses independent applications.
  • packages: Stores shared Dart/Flutter modules.
  • widgetbook: Incorporates the Widgetbook configurations and previews.
monorepo_example/
  apps/
    main_app/
    admin_app/
  packages/
    shared_ui/
  widgetbook/

An alternative structure situates the widgetbook within the apps directory. This organization depends on the developer's preference.

However, isolating widgetbook from apps implies its unique purpose — a platform designed exclusively to refine UI development.

2. Orchestrating Dependencies#

Every package or application's pubspec.yaml should specify dependencies, which allows referencing between packages.

For instance, within main_app, widgetbook, or admin_app, the shared_ui can be referenced as:

dependencies:
  shared_ui:
    path: ../../packages/shared_ui

For clarity, the complete pubspec.yaml of the widgetbook directory is illustrated below:

name: widgetbook_workspace # cannot be named just "widgetbook"
description: A playground to streamline shared_ui with Widgetbook
publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: ">=3.1.0 <4.0.0"

dependencies:
  flutter:
    sdk: flutter
  shared_ui:
    path: ../packages/shared_ui

flutter:
  uses-material-design: true

This configuration facilitates the sharing of shared_ui across multiple projects, optimizing it further with Widgetbook during the development phase.

3. Creating a Widget#

For a hands-on experience, let's formulate a custom button within shared_ui:

import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
  const CustomButton({
    super.key,
    required this.text,
    required this.onPressed,
    this.color = Colors.blue,
    this.textColor = Colors.white,
    this.borderRadius = 8.0,
  });

  final String text;
  final VoidCallback onPressed;
  final Color color;
  final Color textColor;
  final double borderRadius;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        foregroundColor: textColor,
        backgroundColor: color,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(borderRadius),
        ),
      ),
      onPressed: onPressed,
      child: Text(text),
    );
  }
}

4. Configuring Widgetbook#

Within the widgetbook directory, update the pubspec.yaml to include the requisite dependency and dev_dependencies, ensuring you incorporate shared_ui:

dependencies:
  flutter:
    sdk: flutter
  widgetbook_annotation: ^{{ versions.annotation }}
  widgetbook: ^{{ versions.widgetbook }}
  shared_ui:
    path: ../../packages/shared_ui

dev_dependencies:
  widgetbook_generator: ^{{ versions.generator }}
  build_runner:

5. Showcasing Widget Previews#

Go into widgetbook/lib to commence drafting use cases and embarking on other widgetbook-focused assignments.

For our earlier defined CustomButton within the shared_ui library, we can craft two distinct use cases:


// lib/custom_button.dart <- File name

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

@widgetbook.UseCase(
  name: 'with green color',
  type: CustomButton,
)
Widget greenContainerUseCase(BuildContext context) {
  return Center(
    child: CustomButton(
      text: 'Text',
      onPressed: () {},
      color: Colors.green,
    ),
  );
}

@widgetbook.UseCase(
  name: 'with red color',
  type: CustomButton,
)
Widget redContainerUseCase(BuildContext context) {
  return Center(
    child: CustomButton(
      text: 'Text',
      onPressed: () {},
      color: Colors.red,
    ),
  );
}

To render your widgets, create an app within the main.dart file:

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());
}

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

  @override
  Widget build(BuildContext context) {
    return Widgetbook.material(
      // Use the generated directories variable
      directories: directories,
      addons: [],
    );
  }
}

6. Launching Widgetbook#

Navigate to the widgetbook directory and run:

flutter pub run build_runner watch

This command initiates the generator in real-time. Subsequently, execute:

flutter run

This will showcase the CustomButton widget in the Widgetbook environment.

Awesome, this was just the beginning, now you can start building upon this approach.