Getting Started with Widgetbook: A Guide to UI Review and Documentation
If as a Flutter developer, you have ever wanted to display and verify that your implementation of a designed UI is same with what was designed, or you have ever wanted to separately test out how components look in different state without compromising on code structure, then Widgetbook has got you covered.
Table Of Contents
Introduction
Before we go rambling about Widgetbook and its uses, we have to understand what it does or solves, or more precisely, we have to understand what UI documentation is. To do that, we have to understand some terms.
- Documentation: Documentation is the process of creating, organizing, and maintaining written or digital information about a subject or project. It involves recording and presenting information in a clear, concise, and accurate manner to make it accessible to the intended audience. It ensures information is communicated clearly, that knowledge is transferred between team members, and that processes and procedures can be replicated consistently over time.
- UI Documentation: UI documentation is the process of documenting the visual design of a user interface (UI) in a clear and understandable way. It typically involves creating documentation that explains how the UI is laid out, how different components interact with one another, and how the UI is expected to behave in different scenarios.
Now that we have this out of the way, we can begin rambling about Widgetbook
Widgetbook is a UI component development environment for flutter developers. It allows developers create and showcase UI components in an isolated environment and test them outside the context of a full app. This helps the development process on different phases to test UI components with different properties such as devices/screen sizes, themes, orientations and confirm responsiveness.
It is the flutter counterpart for Storybook.js. Yes, it supports add-ons which allow developers to extend the functionality of the tool with additional features, such as custom panel options; it is also a collaborative tool for teams, allowing designers and front-end developers to work together to create high-quality, polished components.
Features of Widgetbook
Widgetbook has a bunch of features that enable us as Flutter developers in our development process to visualize our components(widgets) in isolation and ultimately document our product and its development process. Some of which are:
- Live component documentation: Widgetbook generates a live catalog of your UI components, making it easy to document your widgets and showcase your UI.
- Interactive knobs: Widgetbook’s interactive knobs allow you to tweak the properties of your widgets on the fly, making it easy to experiment with different settings and see how your UI responds in real-time.
- Theming: With Widgetbook, you can switch between different themes to see how your UI looks under different conditions.
- Easy integration with other tools: Widgetbook integrates seamlessly with other development tools like GitHub, making it easy to incorporate into your existing workflow.
- Collaboration: Widgetbook makes it easy to share your UI components with your team members, stakeholders, and even the wider community.
In order to gain a practical understanding of Widgetbook and how to use it effectively, we will be taking a project-based approach. This approach will allow us to see firsthand how to apply Widgetbook in a real-world scenario, and provide us with actionable insights on how to utilize it in our own projects.
Prerequisites: To follow along with this article, you would need
1. good knowledge of flutter
2. familiarity with command-line
3. understanding of annotations in flutterNB: You can find the initial/starting code here.
Project Summary
For this sample project we would be designing a three page application that has an onboarding page, a login page and a home page as shown below, but here we will make it as real as possible, linking each screen and mocking some app setup that each screen should be dependent on.
Here are a few details on the project.
Folder/file Structure
- app
- routes.dart
- locator.dart
- ui
- views
- onboarding_view
- onboarding_view.dart
- onboarding_viewmodel.dart
- login_view
- login_view.dart
- login_viewmodel.dart
- home_view
- home_view.dart
- home_viewmodel.dart
- shared
- home_item.dart
- app_button.dart
- app_textfield.dart
Plugins Used
...
dependencies:
stacked: # State management of choicce
flutter_screenutil: # Responsive design utility(to give our designs more setup)
stacked_services: # Utility package for stacked services, eg navigation
...
dev_dependencies:
build_runner: # To run build runner and generate code
stacked_generator: # Code generator for stacked
App screenshots:
With this, we have an idea of the design we are implementing, and how we can go about it.
Adding Widgetbook to the codebase
Widgetbook has two methods of setting up your UI for review, either by explicitly and manually writing it out; this is suitable for small projects. The second is by making use of Widgetbook annotations, Widgetbook generator and build runner. With these packages, we won’t have to spend much time writing each widget; this method makes it easier to setup and maintain.
Since we are simulating a large app, we will be going with the second method.
Now we open our pubspec.yaml
and make two groups of changes
1. We add widgetbook_annotations
to the dependencies.
2. We add widgetbook
and widgetbook_generator
to the dev_dependencies.
In the end, our pubspec.yaml
should have the following updates.
dependencies:
...
widgetbook_annotation:
dev_dependencies:
...
build_runner:
widgetbook:
widgetbook_generator:
Widgetbook Organizers
Widgetbook allows us to arrange our UI based on predefined organizers. Organizers, in this case, are widgets that helps developers manage their UI components or catalogs. Think of it as a helpers to group and arrange your UI components. These organizers are:
- WidgetbookCategory
- WidgetbookFolder
- WidgetbookComponent
- WidgetbookUseCase
From the documentation:
Both
WidgetbookCategory
andWidgetbookFolder
can contain sub folders andWidgetbookComponent
elements. However,WidgetbookComponent
can only containWidgetbookUseCases
.
These organizers can be used explicitly when using the first method, but like I mentioned earlier, we are making use of the second method(widgetbook_annotations
and widgetbook_generator
). This will create a widgetbook reference of our widget and arrange them according to the folder structure on the Widgetbook dashboard.
Note: Widgetbook dashboard here refers to the resulting application that happens when you run the widgetbook code.
The aim here is to catalogue all views and widgets in the project.
The first step would be register our app by annotating our Root app with @Widgetbook.material()
. This can be switched between (Widgetbook.cupertino()
and WidgetbookApp()
depending on whether you have a material, cupertino or a custom theme).
@WidgetbookApp.material(
name: "Widgetbook Test",
devices: [
Apple.iPhone13ProMax,
Samsung.s21ultra,
Desktop.desktop1080p,
],
)
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: (context, child) {
return MaterialApp(
title: 'Widgetbook Test',
debugShowCheckedModeBanner: false,
theme: getLightTheme(),
darkTheme: getDarkTheme(),
navigatorKey: StackedService.navigatorKey,
onGenerateRoute: StackedRouter().onGenerateRoute,
);
},
);
}
}
@WidgetbookTheme(name: "Light theme", isDefault: true)
ThemeData getLightTheme() {
return ThemeData(
scaffoldBackgroundColor: Colors.white,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
iconTheme: IconThemeData(color: Colors.black),
),
colorScheme: const ColorScheme.light(
secondary: Colors.black,
primary: Colors.black,
),
);
}
@WidgetbookTheme(name: "Dark theme")
ThemeData getDarkTheme() {
return ThemeData(
scaffoldBackgroundColor: Colors.black,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.black,
iconTheme: IconThemeData(color: Colors.white),
),
colorScheme: const ColorScheme.dark(
secondary: Colors.white,
primary: Colors.white,
),
);
}
After registering our app, we can annotate each of our views(a sample of the onboarding view is shown)
@WidgetbookUseCase(name: 'Onboarding View', type: OnboardingView)
Widget onboarding2ViewUseCase(BuildContext context) {
return const OnboardingView();
}
class OnboardingView extends StatelessWidget {
const OnboardingView({super.key});
@override
Widget build(BuildContext context) {
return ViewModelBuilder.nonReactive(
viewModelBuilder: () => OnboardingViewModel(),
builder: (context, model, _) {
return Scaffold(
body: Column(
...
Here, we make use of WidgetbookUseCase
annotation which takes in two arguments; A title of the view that will show on your Widgetbook dashboard and the type of the data you want to display, this will be used to group widgets of similar type.
NB: A function has to be created and then annotated. This is according to Widgetbook syntax and also to simulate a builder function(i.e. a function that takes in a BuildContext argument and returns a widget.
A similar thing can be done for the rest of the views.
We then move to the shared
folder, we annotate the Widget similar to how we did for the views, but since these widgets have arguments, we can make it interactive so we can pass data to it from our Widgetbook console.
Passing data to a use case
To achieve this we make use of Knobs. Knobs are UI controls that allow you to interact with and modify the properties of your widgets in real-time. They can be used to adjust properties such as colour, size, and text content, among others.
We start with the app_button.dart
file, and make the following changes to it.
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:widgetbook/widgetbook.dart' hide WidgetbookUseCase;
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
@WidgetbookUseCase(name: 'App Button', type: AppButton)
Widget appButtonUseCase(BuildContext context) {
return Center(
child: AppButton(
label: context.knobs.text(
label: "Label",
initialValue: "Test Button",
description: "The label of this button",
),
hasBorder: context.knobs.boolean(label: "Has border"),
borderRadius:
context.knobs.nullableNumber(label: "Border Radius")?.toDouble(),
labelSize: context.knobs
.nullableNumber(label: "Label size", initialValue: null)
?.toDouble(),
isCollapsed: context.knobs.boolean(label: "Is collapsed"),
prefixWidget: context.knobs.options(label: "Prefix Widget", options: [
const Option(label: "None", value: null),
const Option(label: "Forward Icon", value: Icon(Icons.arrow_forward)),
]),
suffixWidget: context.knobs.options(label: "Suffix Widget", options: [
const Option(label: "None", value: null),
const Option(label: "Forward Icon", value: Icon(Icons.arrow_forward)),
]),
),
);
}
class AppButton extends StatelessWidget {
final String label;
final VoidCallback? onTap;
final Color? labelColor, buttonColor, borderColor, disabledColor;
final double? width, height, borderRadius, labelSize;
...
Here, we assign different knobs to each argument depending on the type of the argument. For String, we use text knob, for double, we use the number knob and also convert it to double to keep it type safe, use boolean for bool types, etc. But for options where the data type does not have a knob, we use the option knob as we did in prefixWidget
, suffixWidget
and fontWeight
.
NB: 1. All knob types have a description argument, this can be used to describe the use of the knob.
2. We hidWidgetbookUseCase
(on Line 3) in the import statement to avoid import conflicts.
We do something similar to the other two widgets in the shared folder. We have app_textfield.dart
updated to:
@WidgetbookUseCase(name: 'App TextField', type: AppTextField)
Widget appTextFieldViewUseCase(BuildContext context) {
return Center(
child: AppTextField(
hint: context.knobs.text(label: "Hint"),
initialValue: context.knobs.text(label: "Initial Value"),
obscureText: context.knobs.boolean(label: "Obscure text"),
minLines: context.knobs
.nullableNumber(label: "Min Lines", initialValue: 1)
?.toInt(),
maxLines: context.knobs
.nullableNumber(label: "Max Lines", initialValue: 1)
?.toInt(),
enabled: context.knobs.boolean(label: "Is Enabled", initialValue: true),
prefix: context.knobs.options(label: "Prefix Widget", options: [
const Option(label: "None", value: null),
const Option(label: "Search Icon", value: Icon(Icons.search)),
]),
textCapitalization:
context.knobs.options(label: "Text Capitalization", options: [
const Option(label: "None", value: TextCapitalization.none),
const Option(label: "words", value: TextCapitalization.words),
]),
),
);
}
class AppTextField extends StatelessWidget {
final String? hint;
final String? initialValue;
final bool obscureText;
final TextEditingController? controller;
...
And home_item.dart
updated to:
import 'package:widgetbook/widgetbook.dart' hide WidgetbookUseCase;
import 'package:widgetbook_annotation/widgetbook_annotation.dart';
@WidgetbookUseCase(name: 'Home Item', type: HomeItem)
Widget homeItemViewUseCase(BuildContext context) {
return Center(
child: HomeItem(
body: context.knobs.text(label: "Body"),
tag: context.knobs.text(label: "Tag"),
isLiked: context.knobs.boolean(label: "Is Liked"),
price: context.knobs.text(label: "Price"),
isAdded: context.knobs.boolean(label: "Is Added"),
image: context.knobs.options(label: "Image", options: [
const Option(label: "Image 1", value: "assets/Frame 2.png"),
const Option(label: "Image 2", value: "assets/Frame 5.png"),
]),
),
);
}
class HomeItem extends StatelessWidget {
final String image;
final String body;
...
Now that we are done setting up Widgetbook annotations for all our widgets, we start the process of viewing and testing what we have done.
We go to our terminal and run the command
flutter pub run build_runner build --delete-conflicting-outputs
This creates a new file in the root lib folder with the name main.widgetbook.dart
containing the generated code of our setup earlier. We then set our target device to desktop(or mac) and run this code in our terminal.
As at the time of writing this, Widget book is optimized to run on MacOS, Windows, Linux and Web.
flutter run -t lib/main.widgetbook.dart
When we run this, we notice that all the screens have errors, this is because there are some setup we are yet to do. They are:
- Setting up services from
stacked
and stacked services. - Initializing the screen utilities from
flutter_screenutil
package.
Adding Parent Setup To Each View
To fix this, we make use of one more annotation, i.e. WidgetbookAppBuilder
. The builder
parameter in the MaterialApp
widget is a callback function that is used to modify the BuildContext
before it is passed to the MaterialApp
widget's children. It allows you to customize the widget tree for your app by adding additional widgets or modifying existing ones.
In our case, we will use WidgetbookAppBuilder
annotation to initialize the service and screen utilities before each widget is built. To do that, we append this code to the main.dart
...
@WidgetbookAppBuilder()
Widget myAppBuilder(BuildContext context, Widget widget) {
if (!locator.isRegistered<NavigationService>()) {
setupLocator();
}
ScreenUtil.init(
context,
designSize: const Size(375, 812),
);
return widget;
}
Here, we check if the service is registered and register it if it is not. This is to avoid service already registered
error when each widget builds. Also we initialize screen utilility for each widget.
We then re-run build_runner
using the command from earlierflutter pub run build_runner build — delete-conflicting-outputs
and hot restart the application. We then see that each screen comes up, we are able to switch device frames, themes, as well as check different orientations, etc. The view should then look like this.
In the first image, we get a preview of different themes available for our application’s home view. The second image shows a preview of the home view on different devices and screen sizes. Lastly, the third image showcases how we can utilize the knobs we set up earlier to manipulate the values passed into a text field widget and observe how it displays it.
With these practical demonstrations, we have gained a better understanding of the potential of Widgetbook.
Benefits of Widgetbook
Widgetbook provides several benefits for Flutter developers and can be a useful tool for UI development and documentation. Some of the benefits include:
- Rapid UI prototyping: Widgetbook allows developers to quickly prototype and experiment with different UI designs by providing an interactive playground where widgets can be easily modified and previewed.
- Improved collaboration: Widgetbook allows developers, designers, and stakeholders to collaborate more effectively by providing a shared platform where UI designs can be reviewed, tested, and approved.
- Streamlined documentation: With Widgetbook, developers can generate high-quality documentation for their UI components, making it easier for other developers to understand and reuse those components in their own projects.
- Improved code structure: Since Widgetbook helps preview widget components, it encourages good code practices such as separation of concerns and loose coupling.
- Consistency across platforms: Because Widgetbook uses Flutter’s widgets, developers can ensure consistency across different platforms by reusing the same UI components.
- Reduced development time: By providing a fast and intuitive way to prototype UI designs, Widgetbook can help developers reduce the time it takes to build and iterate on UI components.
Conclusion
In conclusion, Widgetbook is a powerful tool for creating and documenting UI components in Flutter. It provides a simple and efficient way to design and preview widgets in isolation, making it easy to experiment with different configurations and visualize how they would look in the app. Additionally, the ability to export and share stories makes collaboration and code sharing a breeze. Whether you are a solo developer, a member of a team, or an open-source contributor, Widgetbook can help streamline your UI development process and improve the quality of your code.
With Widgetbook, creating functional and testable UI components has never been easier. Keep exploring the many features and capabilities of Widgetbook to unlock its full potential and take your UI development to the next level.
If you have any questions or comments about this article, please do not hesitate to reach out to me on Twitter at @lord_chris or LinkedIn at @lord_chris.
UI Inspiration by Patrick Adanini.
Link to full code here.