Chapters

Hide chapters

Real-World Flutter by Tutorials

First Edition · Flutter 3.3 · Dart 2.18 · Android Studio or VS Code

Real-World Flutter by Tutorials

Section 1: 16 chapters
Show chapters Hide chapters

11. Creating Your Own Widget Catalog
Written by Vid Palčar

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

By now, you’re probably well aware that Flutter is all about the widgets. In the past few chapters, you’ve seen that a specific project can consist of hundreds — even thousands — of widgets, which can quickly get out of control. In this chapter, you’ll learn about efficiently managing and maintaining all the widgets in real-world projects.

Duplicating your code seems to save you time… until you need to make a change. Now, you need to find all the places you used that code and make the changes over and over again. And if you miss any, it could cause serious and hard-to-diagnose problems. Flutter, on the other hand, lets you write a widget once, reuse it throughout your code, and have just one place to make any necessary changes. Easy!

Reusing already-created widgets can save you a lot of time and effort when creating and maintaining your projects. Many teams reuse widgets not only within a project, but even across numerous apps. That practice allows you to keep maintenance efforts low and makes the process of unifying the brand identity over multiple projects easier than ever. Having a component library with a storybook can be an invaluable tool for reusability of UI components. Although the two terms are often used interchangeably, you’ll learn about the differences between them.

A component library is a package that consists of fairly small components: widgets. You use these widgets as building blocks when creating custom UIs across one or multiple apps. A storybook, on the other hand, allows you to present the components you’ve built to your fellow team members, product managers and designers. It allows them to better understand how a specific component/widget will render across multiple devices and orientations.

Since a component library is a separate package, you can easily use it in multiple products or share it with the world by publishing it to pub.dev. You can even add an example app to it. In your case, this storybook will run across multiple devices as a standalone app.

In this chapter, you’ll learn:

  • Reasons you need a component library and storybook.
  • How to create a reusable component and add it to the component library.
  • How to add a standalone example app to a package.
  • The basic structure of a storybook.
  • How to customize a storybook.

Throughout this chapter, you’ll work on the starter project from this chapter’s assets folder.

Why Do You Need a Component Library?

You might be familiar with object-oriented programming (OOP), and widgets are one of the ways to implement it in Flutter. Taking it one step further, component libraries are a real-world approach to taking OOP across modules, apps, organizations and the external world.

Each component/widget in the component library acts like a small building block that you can use to create a more complex custom UI implementation. Breaking code into smaller components allows you to apply quick modifications to the UI with minimal effort. Remember the pain of reimplementing a component — or even a screen — for the fifth time because the design team came up with a brilliant new idea again?

Rather than going through all the appearances of the design feature in your app, you only have to modify a specific attribute of the widget or simply replace it with a new one. In no time, you’re back on track — working on things that really matter.

Furthermore, a single widget for a specific purpose in the app gives the feeling of consistency in the design language. That increases the overall UI quality, which reflects higher user satisfaction.

Customizing a Specific Component

Now that you have a better understanding of what a component library is, it’s time to look at how you implement reusable components in the WonderWords app. Open the starter project and run the app. In the app, navigate to the login screen and look at the design of the Sign In button:

// 1
child: icon != null
    // 2
    ? ElevatedButton.icon(
        onPressed: onTap,
        label: Text(
          label,
        ),
        icon: icon,
      )
    // 3
    : ElevatedButton(
        onPressed: onTap,
        child: Text(
          label,
        ),
      ),

Adding Components to the Component Library

Go to quote_details_screen.dart located in features/quote_details/lib/src, and look at the following code snippet for a moment:

// TODO: replace with centered circular progress indicator
const Center(
    child: CircularProgressIndicator(),
  ),
// TODO: replace with centered circular progress indicator
return const Center(
  child: CircularProgressIndicator(),
),
import 'package:flutter/material.dart';

class CenteredCircularProgressIndicator extends StatelessWidget {
  const CenteredCircularProgressIndicator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: CircularProgressIndicator(),
    );
  }
}
export 'src/centered_circular_progress_indicator.dart';
const CenteredCircularProgressIndicator(),
return const CenteredCircularProgressIndicator();

Why Do You Need a Storybook?

In learning about the component library, you’ve learned a good practice of writing maintainable, reusable code. But now, imagine the following situation: Suppose you have to build a new feature. To avoid duplicating the code, you have to check whether you or your fellow team members have already implemented a specific design feature. That can get very time-consuming as the project grows. By using a storybook, you can check if the specific component already exists as part of the components_library package, as well as modify some of its attributes to make sure it fulfills all the design requirements. You can also see widgets in different form factors as well as dark and light mode appearances in the cases when you’ve defined it for your widgets.

Adding a Storybook to a Flutter App

A few open-source solutions allow you to add a customizable storybook to your component library. In this chapter, you’ll use storybook_flutter, as it stands out with the support of multiple features, such as localization, dark mode, device previews and others. It also has a knob panel, which allows you to adjust predefined attributes of a specific widget when testing it in real time.

storybook_flutter: ^0.8.0

Basic Structure of Storybook UI

To give you a sneak preview and better understanding of the topic, here’s what the WonderWords storybook will look like at the end of this chapter after running it in the browser:

Device Preview and Theming

In the image above, the top circled icon on the right enables you to show the current story in a device preview. You can select a device from a predefined set of devices and change its orientation.

Storybook UI on a Mobile App

You already know that you can run the storybook on all three main platforms supported by Flutter: iOS, Android and web. All the storybook panels are also present on its mobile version, with the layout adjusting to the smaller screen size. Check it out:

Making the Storybook App Runnable

In the terminal, navigate to the example folder of component_library with the following command:

$ cd packages/component_library/example
$ flutter run
Target file "lib/main.dart" not found.
import 'package:flutter/material.dart';
// TODO: add missing import

void main() {
  runApp(
    // TODO: replace the MaterialApp placeholder later
    MaterialApp(
      home: Container(color: Colors.grey),
    ),
  );
}
No supported devices connected.
The following devices were found, but are not supported by this project:
sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64  • Android 13 (API 33) (emulator)
macOS (desktop)             • macos         • darwin-arm64   • macOS 12.6 21G115 darwin-arm
Chrome (web)                • chrome        • web-javascript • Google Chrome 105.0.5195.125
If you would like your app to run on android or macos or web, consider running `flutter create .` to generate projects
for these platforms.
Recreating project ...
...
...
Wrote 122 files.

All done!

In order to run your application, type:

  $ cd .
  $ flutter run

Your application code is in ./lib/main.dart.

Understanding Component Storybook

Now that you can successfully run the storybook app, you’ll inspect what the source code from the lib folder does. First, open component_storybook.dart and look at the build() method of a ComponentStorybook widget. In the next few subsections, you’ll learn how to configure a storybook for your needs.

Specifying Stories and an Initial Story

As mentioned, a storybook is a collection of stories — or components — put together in an organized order. Take a closer look at its only required attribute, children, and the initialRoute attribute:

// 1
children: [
  ...getStories(theme),
],
// 2
initialRoute: 'rounded-choice-chip',

Specifying Themes

The first two attributes of the storybook widget are theme and darkTheme:

@override
Widget build(BuildContext context) {
  // 1
  final theme = WonderTheme.of(context);
  return Storybook(
    // 2
    theme: lightThemeData,
    // 3
    darkTheme: darkThemeData,
    // TODO: add localization delegates
    children: [
      ...getStories(theme),
    ],
  );
}

Specifying Localization

Next, you have to locate the localizationDelegates attribute in the storybook widget by replacing // TODO: add localization delegates with the following code snippet:

localizationDelegates: const [
  GlobalMaterialLocalizations.delegate,
  GlobalWidgetsLocalizations.delegate,
  GlobalCupertinoLocalizations.delegate,
  ComponentLibraryLocalizations.delegate,
],

Applying StoryApp to runApp() Methods

Lastly, replace // TODO: replace the MaterialApp placeholder later in main.dart’s StoryApp with:

StoryApp()
import 'package:component_library_storybook/story_app.dart';
import 'package:component_library_storybook/story_app.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(StoryApp());
}
@override
  Widget build(BuildContext context) {
    return WonderTheme(
      lightTheme: _lightTheme,
      darkTheme: _darkTheme,
      child: ComponentStorybook(
        lightThemeData: _lightTheme.materialThemeData,
        darkThemeData: _darkTheme.materialThemeData,
      ),
    );
  }

Understanding a Story

You can create a Story a couple different ways — a simple story and a complex story. Choosing the right way varies from case to case, and it’s important from the perspective of how informative your Story will be for the end user. In the case of Storybook, the end user might be a fellow developer, your lead UI designer or even a client. So, it has to be as configurable as possible.

Defining a Simple Story

A simple Story is used in cases when the widget/component has no configuration options. You can figure out whether a specific widget should have configuration options by looking at its attributes. For example, among all the widgets present in component_library, ShareIconButton, LoadingIndicator, SearchBar and RowAppBar are simple widgets. Such widgets don’t have any attributes that would define the way they should render.

// 1
Story.simple(
  name: 'Simple Expanded Elevated Button',
  section: 'Buttons',
  // 2
  child: ExpandedElevatedButton(
    label: 'Press me',
    onTap: () {},
  ),
  // TODO: add additional attributes to the story later
),
padding: const EdgeInsets.all(64.0),
background: Colors.cyanAccent,

Defining a Complex Story

complex Story isn’t much different from simple Story. The only difference between the two is that complex Story uses builder instead of child. Using builder enables you to configure the knob panel for a widget. For example, look at the ExpandedElevatedButton widget that has the following fields:

final String label;
final VoidCallback? onTap;
final Widget? icon;
Story(
  name: 'Expanded Elevated Button',
  section: 'Buttons',
  builder: (_, k) => ExpandedElevatedButton(
    label: k.text(
      label: 'label',
      initial: 'Press me',
    ),
    onTap: k.boolean(
      label: 'onTap',
      initial: true,
    )
        ? () {}
        : null,
    icon: Icon(
      k.options(
      label: 'icon',
        initial: Icons.home,
        options: const [
          Option(
            'Login',
            Icons.login,
          ),
          Option(
            'Refresh',
            Icons.refresh,
          ),
          Option(
            'Logout',
            Icons.logout,
          ),
        ],
      ),
    ),
  ),
),

Adding a Knob for Text

Now, you’ll take the RoundedChoiceChip story in the stories.dart file as a reference. You might notice it has a k.text method:

label: k.text(
  // 1
  label: 'label',
  // 2
  initial: 'I am a Chip!',
),

Adding a Knob for a Boolean

In the same RoundedChoiceChip story, also notice a k.boolean method:

isSelected: k.boolean(label: 'isSelected', initial: false),

Adding a Knob for int or double

Look at storybook on your mobile simulator or in your browser, and locate the Upvote Icon Button story under the Counter Indicator Buttons tile in the app. Go to the knob panel, and you’ll see a slider to change the vote count. That’s the knob for the int or double type field:

count: k.sliderInt(
  label: 'count',
  max: 10,
  min: 0,
  initial: 0,
  divisions: 9,
),
k.slider(
  label: 'count',
  max: 10,
  min: 0,
  initial: 0,
),

Adding a Knob for Custom Types

So far, you’ve seen how to add knobs for primitive data types. Now, it’s time to learn about the wildcard knob, which you can use for any custom type. Look back at the example of RoundedChoiceChip, which has customizable colors. The color type isn’t a primitive Dart data type. In such scenarios, you should use k.options, as shown below:

backgroundColor: k.options(
  label: 'backgroundColor',
  initial: null,
  options: const [
    Option('Light blue', Colors.lightBlue),
    Option('Red accent', Colors.redAccent),
  ],
),

Custom Wrapper for a Story

The storybook has one more customization option worth mentioning. You can add a custom wrapper to every single Story widget using wrapperBuilder for Story. So, suppose you want to see how the QuoteCard widget renders itself in ListView. You can easily achieve this by using wrapperBuilder here. Add the following code snippet to a complex Story with Quotes in List in stories.dart. To locate it easier, search for // TODO: add wrapper builder for quotes list:

// 1
wrapperBuilder: (context, story, child) => Padding(
  padding: const EdgeInsets.all(8.0),
  child: ListView.separated(
    itemCount: 15,
    // 2
    itemBuilder: (_, __) => child,
    separatorBuilder: (_, __) => const Divider(height: 16.0),
  ),
),

Challenge

You accomplished a lot in getting through this chapter, and this is an excellent opportunity for you to test your knowledge.

Key Points

  • A storybook is a visual representation of the component library.
  • A storybook can be a separate app or an integral part of your main app.
  • Use the flutter create . command to add platform-specific folders.
  • Configure the knob panel for fields you feel the user would like to change and test.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now