Flutter 中使用 Widgetbook 管理你的元件

會煮咖啡的貓發表於2023-05-06

Flutter 中使用 Widgetbook 管理你的元件

<img src="https://ducafecat.oss-cn-beijing.aliyuncs.com/podcast/2023/05/ad04470d9d20876ab34c1b36b08cf482.jpeg" style="width:90%;" />

前言

Flutter 介面開發中我們有幾個痛點 :

  • 與設計師協作複用一套設計規範(figma)
  • 視覺化的管理你的元件程式碼(基礎元件、業務元件)
  • 不同裝置尺寸測試你的元件
  • 實時修改你的測試元件引數
原文 https://ducafecat.com/blog/flutter-uses-a-widgetbook-to-manag...

影片

https://www.bilibili.com/video/BV1qM4y1b7WL/

參考

Widgetbook

https://www.widgetbook.io/

<img src="https://ducafecat.oss-cn-beijing.aliyuncs.com/podcast/2023/05/6840ba9ac214ee1f2938c79ed5fab5a6.png" style="width:80%;" />

Flutter Widgetbook 是一個用於構建和互動 Flutter 元件庫的工具。它允許您在單獨的應用程式中構建和演示您的 Flutter 元件,以便您可以在不執行完整應用程式的情況下進行快速迭代和測試。

使用 Flutter Widgetbook,您可以:

  • 構建和演示單個元件,而無需在完整應用程式中執行它們。
  • 以互動方式測試元件的不同狀態和屬性,以及不同平臺和裝置的外觀和行為。
  • 共享您的元件庫,並讓其他人輕鬆地檢視和測試您的元件。

您可以在 Flutter 應用程式中使用 Widgetbook,也可以將其作為獨立應用程式使用。在 Widgetbook 中,您可以編寫 Dart 程式碼來定義元件和演示它們的用法。您可以使用 Flutter 提供的任何元件和庫,並使用 Widgetbook 提供的一些工具來組織和顯示您的元件。

設計規範

前端設計規範是一組定義前端設計和開發過程中所需遵守的準則和規則的規範。它們旨在確保前端程式碼的一致性、可維護性、可擴充套件性和可重用性,並促進團隊間的協作。

前端設計規範主要包括以下內容:

  1. 佈局規範:定義頁面佈局和排版的規則,包括網格系統、排版間距、基準線等。
  2. 樣式規範:定義顏色、字型、圖示、按鈕等基本樣式的使用和規範,包括設計風格、調色盤、字型型別、字號、行高等。
  3. 元件規範:定義前端元件的設計和開發規則,包括元件的命名、結構、樣式、互動、狀態管理等。
  4. 圖片和媒體規範:定義圖片和媒體資源的格式、尺寸、最佳化和載入等規則,以提高頁面效能和使用者體驗。
  5. 響應式設計規範:定義響應式設計的原則和規則,包括頁面佈局、元素大小和位置、字型大小、圖片和媒體資源的顯示等。
  6. 可訪問性規範:定義網站或應用程式的可訪問性規則,包括鍵盤導航、語義標記、焦點指示、顏色對比度等。
  7. 效能規範:定義最佳化前端效能的規則,包括程式碼壓縮、快取控制、資源載入、程式碼分割等。

前端設計規範可以透過檔案、工具、模板和程式碼庫等方式來實現和維護。它們可以幫助團隊提高開發效率、降低維護成本、保持程式碼質量和可維護性,並促進設計和開發間的協作。

Ant Design 設計規範參考

https://ant.design/docs/spec/introduce-cn

<img src="https://ducafecat.oss-cn-beijing.aliyuncs.com/podcast/2023/05/54cef4391c97f0e4b4820b0b9b1c4232.png" style="width:80%;" />

程式碼

https://github.com/ducafecat/flutter_develop_tips/tree/main/flutter-widgetbook

步驟

安裝 widgetbook 元件

pubspec.yaml

dev_dependencies:
  flutter_test:
    sdk: flutter

  ...

  widgetbook: ^3.0.0-beta.14
注意是放在 dev_dependencies 節點下面

編寫除錯介面

lib/app.widgetbook.dart

// ignore_for_file: depend_on_referenced_packages

import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_in_flutter_course/widgets/button.dart';

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

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

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

執行

<img src="https://ducafecat.oss-cn-beijing.aliyuncs.com/podcast/2023/05/d62fe3adfa8a115e710f6595995abcd8.png" style="width:80%;" />

加入元件

準備兩個元件程式碼

lib/widgets/button.dart

import 'package:flutter/material.dart';

class MyElevatedButton extends StatelessWidget {
  final VoidCallback? onPressed;
  final String? text;
  final IconData? icon;
  final Color? textColor;
  final Color? buttonColor;
  final double? borderRadius;
  final double? height;
  final double? width;

  const MyElevatedButton({
    Key? key,
    this.onPressed,
    this.text,
    this.icon,
    this.textColor,
    this.buttonColor,
    this.borderRadius,
    this.height,
    this.width,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: height ?? 48.0,
      width: width,
      child: ElevatedButton(
        onPressed: onPressed,
        style: ElevatedButton.styleFrom(
          primary: buttonColor ?? Theme.of(context).primaryColor,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(borderRadius ?? 8.0),
          ),
        ),
        child: icon == null
            ? Text(
                text!,
                style: TextStyle(color: textColor ?? Colors.white),
              )
            : Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(icon, color: textColor ?? Colors.white),
                  SizedBox(width: 8.0),
                  Text(
                    text!,
                    style: TextStyle(color: textColor ?? Colors.white),
                  ),
                ],
              ),
      ),
    );
  }
}

lib/components/login.dart

import 'package:flutter/material.dart';

class LoginForm extends StatefulWidget {
  const LoginForm({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  bool _isObscured = true;
  bool _isLoading = false;

  void _toggleObscure() {
    setState(() {
      _isObscured = !_isObscured;
    });
  }

  void _submit() async {
    if (_formKey.currentState!.validate()) {
      setState(() {
        _isLoading = true;
      });

      // Simulate a login request
      await Future.delayed(const Duration(seconds: 2));

      setState(() {
        _isLoading = false;
      });

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Logged in successfully!'),
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Form(
        key: _formKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            TextFormField(
              controller: _emailController,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                labelText: 'Email',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your email';
                }
                if (!value.contains('@')) {
                  return 'Please enter a valid email address';
                }
                return null;
              },
            ),
            const SizedBox(height: 16),
            TextFormField(
              controller: _passwordController,
              obscureText: _isObscured,
              decoration: InputDecoration(
                labelText: 'Password',
                suffixIcon: IconButton(
                  icon: Icon(
                    _isObscured ? Icons.visibility : Icons.visibility_off,
                  ),
                  onPressed: _toggleObscure,
                ),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your password';
                }
                if (value.length < 6) {
                  return 'Password must be at least 6 characters long';
                }
                return null;
              },
            ),
            const SizedBox(height: 32),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _isLoading ? null : _submit,
                child: _isLoading
                    ? const CircularProgressIndicator()
                    : const Text('Log in'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

分類1 - 公共元件

  WidgetbookCategory buildWidgetbookCategory() {
    return WidgetbookCategory(
      name: '公共元件',
      children: [
        WidgetbookComponent(
          name: '按鈕',
          useCases: [
            WidgetbookUseCase.center(
              name: "紅色背景",
              child: MyElevatedButton(
                onPressed: () => print("Button pressed"),
                text: "Click me",
                icon: Icons.arrow_forward,
                buttonColor: Colors.red,
                borderRadius: 16.0,
                height: 60.0,
                width: double.infinity,
              ),
            ),
            WidgetbookUseCase.center(
              name: "藍色色背景",
              child: MyElevatedButton(
                onPressed: () => print("Button pressed"),
                text: "Click me",
                icon: Icons.arrow_forward,
                buttonColor: Colors.blue,
                borderRadius: 16.0,
                height: 60.0,
                width: double.infinity,
              ),
            )
          ],
        ),
      ],
    );
  }

分類2 - 業務元件

  WidgetbookCategory buildWidgetbookCategory2() {
    return WidgetbookCategory(
      name: '業務元件',
      children: [
        WidgetbookComponent(
          name: '系統常用',
          useCases: [
            WidgetbookUseCase(
              name: "登入介面",
              builder: (BuildContext context) {
                return LoginForm(
                  title: context.knobs.text(
                    label: '標題 [title]',
                    initialValue: '使用者登入',
                  ),
                );
              },
            ),
            WidgetbookUseCase(
              name: "註冊介面",
              builder: (BuildContext context) {
                return LoginForm(
                  title: context.knobs.text(
                    label: '標題 [title]',
                    initialValue: '使用者註冊',
                  ),
                );
              },
            ),
          ],
        ),
      ],
    );
  }
透過 knobs 的方式設定除錯引數

其它引數型別

<img src="https://ducafecat.oss-cn-beijing.aliyuncs.com/podcast/2023/05/b46521f4ef5e8504fecc8286ed5108de.png" style="width:80%;" />

build 函式

  @override
  Widget build(BuildContext context) {
    return Widgetbook.material(
      addons: [
      ],
      directories: [
        // 基礎元件
        buildWidgetbookCategory(),

        // 業務元件
        buildWidgetbookCategory2(),
      ],
    );
  }

輸出

<img src="https://ducafecat.oss-cn-beijing.aliyuncs.com/podcast/2023/05/b5a12d8974454de0967b9dd2b47aa459.png" style="width:80%;" />

設定選項

主題

  MaterialThemeAddon buildMaterialThemeAddon() {
    return MaterialThemeAddon(
        setting: MaterialThemeSetting.firstAsSelected(themes: [
      WidgetbookTheme(name: "dark", data: ThemeData.dark()),
      WidgetbookTheme(name: "light", data: ThemeData.light()),
    ]));
  }

字型尺寸

  TextScaleAddon buildTextScaleAddon() {
    return TextScaleAddon(
        setting: TextScaleSetting.firstAsSelected(
            textScales: [1.0, 1.25, 1.5, 1.75, 2]));
  }

build 函式

  @override
  Widget build(BuildContext context) {
    return Widgetbook.material(
      addons: [
        // 主題
        buildMaterialThemeAddon(),

        // 字型大小
        buildTextScaleAddon(),
      ],
      directories: [
        // 基礎元件
        buildWidgetbookCategory(),

        // 業務元件
        buildWidgetbookCategory2(),
      ],
    );
  }

輸出

<img src="https://ducafecat.oss-cn-beijing.aliyuncs.com/podcast/2023/05/39a52b9f8a7377520be5e5188d80725c.png" style="width:80%;" />

其它 addon

<img src="https://ducafecat.oss-cn-beijing.aliyuncs.com/podcast/2023/05/6904961f63b178731a9fe2d567769ace.png" style="width:50%;" />

小結

Flutter Widgetbook 對前端開發工作有以下好處:

  1. 提高開發效率:Flutter Widgetbook 可以讓前端開發人員在不需要啟動完整應用程式的情況下構建和演示 Flutter 元件,快速迭代和測試元件的不同狀態和屬性,從而提高開發效率。
  2. 促進元件複用:Flutter Widgetbook 可以讓前端開發人員在單獨的應用程式中構建和演示元件,從而促進元件的複用和共享,減少程式碼重複和維護成本。
  3. 保持程式碼一致性:Flutter Widgetbook 可以作為一個元件庫來使用,定義前端元件的設計和開發規則,從而保持程式碼的一致性、可維護性、可擴充套件性和可重用性。
  4. 提高跨團隊協作:Flutter Widgetbook 可以讓前端開發人員共享他們的元件庫,並讓其他人輕鬆地檢視和測試他們的元件,從而促進跨團隊協作和知識共享。
  5. 提高使用者體驗:Flutter Widgetbook 可以讓前端開發人員在不同平臺和裝置上測試元件的外觀和行為,以確保它們能夠提供一致的使用者體驗,從而提高使用者體驗和使用者滿意度。

總之,Flutter Widgetbook 是一個有用的工具,可以幫助前端開發人員更輕鬆地構建和測試 Flutter 元件,從而提高開發效率、保持程式碼質量和可維護性,並促進跨團隊協作和知識共享,最終提高使用者體驗和使用者滿意度。


© 貓哥

ducafecat.com

end

本文由mdnice多平臺釋出

相關文章