Flutter 构建设计系统

原文 https://medium.com/@calpoog/building-design-systems-in-flutter-d52d66004070

前言

Nielson Normal Group (一家用户体验研究咨询公司)对设计系统的定义如下:

设计系统是一套标准,通过减少冗余来管理大规模的设计,同时创建跨不同页面和通道的共享语言和视觉一致性。

与名称所暗示的不同,设计系统的规则不仅仅适用于设计师。一个良好构建和实现的设计系统的真正价值在于将这些规则强加给设计师和开发人员,创造一致性,使得这两个学科能够专注于更高层次的挑战,而忽略构成一个有凝聚力的应用程序的组合部分的单调乏味。

你可能听说过像 Bootstrap 和 antd 这样的设计系统,或者 AirBnB 的设计语言系统。即使他们不会在屋顶上大喊大叫,成功的设计公司也可能拥有一套设计体系。这使得他们可以专注于应用程序的身份和功能,而不必考虑按钮、 banner 、文本等各个部分。作为一个设计师,我不想开始设计一个页面,不得不考虑什么是按钮,并详尽地创建开发人员使用的规范。作为一个开发人员,我不希望看到一个按钮,我必须从头开始构建第 10 次交付设计。一个好的设计系统及其实现的对应物(在我们的例子中,一个 Flutter 库)解决了这些问题。

我们将跳过讨论设计系统是如何形成的,谁制定标准,以及如何对其进行文档化。相反,我们假设我们有一个定义良好的设计系统,称之为 MyDesign。我们的工作是在 Flutter 中构建 MyDesign 的组件,并让它们准确地表示我们的设计系统定义的标准。我们将特别关注一组按钮 widget :

A way-too-simple design system 一个简单的方式设计系统

本文接下来介绍的是我学到的一些技巧和窍门,这些技巧和窍门帮助我们实现了成功的、可伸缩的和可重用的设计系统。

正文

命名很重要

作为开发人员,我们知道语义对于采用和入职非常重要。但在设计系统的情况下,这 extension 到我们友好的邻里设计师。我们正在创建一种共享语言,因此应该使用该语言实现我们的 widget 。

在描述组件时,尽量使用与设计器相同的名称来命名 widget 。这样可以确保在设计和实现之间来回工作时,不会出现错误沟通。也可以将此方法应用于 widget 的配置。如果一个按钮有一个“前导”和“尾随”图标,而不是“左”和“右”,那么在 widget 类的字段中使用相同的术语。在 MyDesign 按钮的例子中,我们可以看到它们反映了“材质填充(提升)”和“轮廓”按钮,但是它们被命名为“主按钮”和“次按钮”。我们的实现应该尊重该命名方案。

通常,设计系统实现将独立于它们所支持的应用程序/s。这是一种有益的分离,因此业务逻辑不会渗透到设计实现中,并且您的库可以作为多个使用应用程序的依赖项重用。我发现,正因为如此,设计系统为 widget 提供的前缀也是有益的。MyButton 而不是 Button 有助于区分应用程序中哪些 widget 是本地的,哪些不是,同时避免与 Flutter 和其他应用程序依赖项提供的许多 widget 发生名称冲突。

独立定义设计令牌 tokens

设计令牌是设计系统的“原语”ーー常量值在整个组件和已定义的标准中重用。像颜色、间距、文本样式和图标这样的东西通常作为“标记”包含在设计系统中,并且是独立于使用它们的 widget 定义的很好的候选者。Material Design 已经使用 Colors 和 Icon 类,以及默认主题对象的 textTheme 来实现这一点,这些主题对象包含预定义的文本处理,比如主体和标题样式。

class MyColors {
  MyColors._();

  static const Color dark = Color(0xff222222);
  static const Color white = Color(0xffffffff);
  static const Color blue = Color(0xff0000ff);
  static const Color red = Color(0xffff0000);
  static const Color green = Color(0xff00ff00);
  static const Color lightBlue = Color(0xffaaaaff);
}

定义一组颜色标记

use composition

Flutter 建立在积极的可组合性的概念之上,你的设计系统库也应该如此。利用 Flutter 的 Material 设计和库比蒂诺 widget 的广泛目录和风格,以符合您的系统的规格。这样可以抽象出所有将要使用 widget 的地方的样式。通过创建可以组合到其他 widget 中的 widget ,避免在 widget 之间重复代码。通过 MyDesign 按钮,我们可以将 Materials 的按钮作为基础。在更复杂的系统中,您可能拥有自己的基本组件,这些组件可以组成多个其他组件。

Reduce the API surface

尽量使您的 widget 尽可能简单。遵循知识最少的原则ーー一个 widget 应该只消耗正确显示和正常运行所需的输入。这也意味着您的 widget 不能以意想不到的方式使用。

如果您正在使用一个 Material widget 并为其设计样式,那么您可能不需要该 widget 允许的所有配置。材质 widget 应该是高度可配置的,但是您的 widget 可能不是。减少那些参数!

有些 widget ,比如 ElevatedButton,可以非常灵活地传递给 child (它需要任何 widget )。在 MyDesign 中,按钮只允许将文本和图标作为子元素,因此我们将重新键入 widget 的字段,以确保只接受有效值,并允许构建函数抽象出构建按钮内部的复杂性。

class MyButton extends StatelessWidget {
  final String? label;
  final IconData? icon;
  final VoidCallback? onPressed;

  const MyButton({
    super.key,
    this.label,
    this.icon,
    this.onPressed,
  }) : assert(label != null || icon != null, 'Label or icon must be provided.');
  // Use asserts to enforce rules which cannot be done at compile time

  
  Widget build(BuildContext context) {
    return ElevatedButton(
      // Pass through parameters which are still necessary
      onPressed: onPressed,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        // Icon, icon + text, and text-only possibilities are abstracted in MyButton
        children: [
          if (icon != null) Icon(icon, size: 18.0),
          if (icon != null && label != null)
            const SizedBox(width: MySpacing.x0L),
          if (label != null) Text(label!, textAlign: TextAlign.center),
        ],
      ),
    );
  }
}

MyDesign 按钮的粗略的第一个实现

使用枚举强制执行有效输入

本技巧是对上述建议的 extension 。在许多情况下, widget 的字段类型可能允许不符合设计系统规则的值。例如,MyDesign 的按钮只允许品牌调色板中的一部分颜色。我们将创建一个枚举(enum)来表示并限制配置,而不是让我们的 widget 使用一个 Color 类型参数(从而允许错误着色的按钮)。

在 Dart 2.17 的增强枚举之前,这会造成一些小麻烦,因为必须使用 Map 或 switch 案例将这些枚举值映射回我们的构造函数或构建函数中的有效类型。但是,使用增强的枚举,我们可以定义枚举,最终字段将每个枚举值连接到其表示的值。

// Restrict color inputs to MyButton using an enum.
enum MyButtonColor {
  red(MyColors.red),
  blue(MyColors.blue),
  green(MyColors.green);

  final Color color;

  const MyButtonColor(this.color);
}

class MyButton extends StatelessWidget {
  final String? label;
  final IconData? icon;
  final MyButtonColor color;
  final VoidCallback? onPressed;

  const MyButton({
    super.key,
    this.label,
    this.icon,
    this.color = MyButtonColor.blue,
    this.onPressed,
  }) : assert(label != null || icon != null, 'Label or icon must be provided.');

  
  Widget build(BuildContext context) {
    return ElevatedButton(
      // A contrived example using styleFrom to build a ButtonStyle from the background color
      style: ElevatedButton.styleFrom(backgroundColor: color.color),
      onPressed: onPressed,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          if (icon != null) Icon(icon, size: 18.0),
          if (icon != null && label != null)
            const SizedBox(width: MySpacing.x0L),
          if (label != null) Text(label!, textAlign: TextAlign.center),
        ],
      ),
    );
  }
}

现在我们的按钮遵循 MyDesign 的颜色规则

Inherit stylings 继承风格

最好不要硬编码值。我们主要通过将设计标记分离为常量来处理这个问题。然而,在整个代码中使用 MyColors.blue 代替 Color (0xff0000ff)仍然会使我们的设计系统受到不必要的限制。虽然我们的标记允许在设计系统需要更改的情况下单独编辑值,但是如果我们需要一个全新的主题呢?这是应用程序中使用明暗主题的常见做法。

静态常量变量不能解决用户对主题的偏好。因此,像材质设计一样,通过修改全局主题的属性或创建主题 extension 来使用来自全局主题的样式继承。

Use named constructors 使用命名构造函数

这可能归结为个人偏好,但是 Dart 的命名构造函数可以帮助减少样板文件并增强 widget 的语义。

看看我们的 MyDesign 主按钮和辅助按钮,我们知道在构建内部结构时有重用的代码,但是我们仍然需要分别使用 Material EleatedButton 和 OutlineButton。我们可以创建两个独立的 widget MyPrimaryButton 和 MySecond daryButton,并提取一个用于构建子部件的 widget 。或者,我们可以有一个带有命名构造函数的 widget ,MyButton.basic 和 MyButton.second。

这些构造函数可以设置一个私有字段,告诉我们在构建方法中要做什么。随着逻辑和构建方法复杂性的增加(代码共享比协调多个 widget 通信更容易) ,这种方法对于多个 widget 更有价值。以这种方式为语义目的对 widget 的变体进行“分组”也很有价值。IDE 代码完成后,键入 MyButton。提供了可用变量的“目录”,这是通过多 widget 方法没有得到的好处。

class MyButton extends StatelessWidget {
  final String? label;
  final IconData? icon;
  final MyButtonColor color;
  final VoidCallback? onPressed;
  final bool _primary;

  const MyButton.primary({
    super.key,
    this.label,
    this.icon,
    this.color = MyButtonColor.blue,
    this.onPressed,
  })  : _primary = true,
        assert(
          label != null || icon != null,
          'Label or icon must be provided.',
        );

  const MyButton.secondary({
    super.key,
    this.label,
    this.icon,
    this.color = MyButtonColor.blue,
    this.onPressed,
  })  : _primary = false,
        assert(
          label != null || icon != null,
          'Label or icon must be provided.',
        );

  
  Widget build(BuildContext context) {
    final child = Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (icon != null) Icon(icon, size: 18.0),
        if (icon != null && label != null) const SizedBox(width: MySpacing.x0L),
        if (label != null) Text(label!, textAlign: TextAlign.center),
      ],
    );

    if (_primary) {
      return ElevatedButton(
        style: ElevatedButton.styleFrom(backgroundColor: color.color),
        onPressed: onPressed,
        child: child,
      );
    } else {
      return OutlinedButton(
        style: OutlinedButton.styleFrom(backgroundColor: color.color),
        onPressed: onPressed,
        child: child,
      );
    }
  }
}

使用命名构造函数对变量 widget 进行分组

小结

你可以随心所欲地对你的设计系统严格要求。在许多情况下,信任开发人员遵守口头或书面规则就足够了。但是,构建利用与设计人员共享的术语并严格执行系统规则的 widget 可以加快设计到开发的切换速度,并使未来的开发人员更容易上手。

结束语

如果本文对你有帮助,请转发让更多的朋友阅读。

也许这个操作只要你 3 秒钟,对我来说是一个激励,感谢。

祝你有一个美好的一天~

猫哥课程


© 猫哥

  • 微信 ducafecat

  • https://wiki.ducafecat.tech

  • https://ducafecat.com

Last Updated:
Contributors: ducafecat