Flutter大型项目架构:UI设计系统实现

发布于:2024-04-25 ⋅ 阅读:(19) ⋅ 点赞:(0)

前面几篇讲了很多关于分层设计、状态管理和依赖管理,但是作为前端开发,设计资源怎么去管理、设计系统如何去实现其实在日常开发中接触是最多的,每个开发者或者项目都有一套自己的管理方式或实现方式,今天来分享一下我在大型项目中是如何做设计和实现资源管理的。

Flutter 默认的设计系统

Flutter 写页面的时候通常会用到 package:flutter/material.dartpackage:flutter/cupertino.dart ,主要是为了使用 Flutter SDK 提供的 Material/Cupertino Design 风格的UI组件和工具,这其中它的默认主题。虽然您可以自定义默认文本主题的标题样式,但被严格限制为 3 个级别:Large, Medium, SmallColor 的命名的变量个数也是有限制的。

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter技术实践',
      theme: ThemeData(
        // 设置主色为蓝色
        primaryColor: Colors.blue,
        // 设置强调色为绿色
        accentColor: Colors.green,
        // 设置默认字体为Roboto
        fontFamily: 'Roboto',
        // 设置默认文本样式
        textTheme: TextTheme(
            // 设置标题文本样式
            displayMedium:
                TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            // 设置正文文本样式
            bodyMedium: TextStyle(fontSize: 16),
        ),
        // 设置按钮的样式
        buttonTheme: ButtonThemeData(
          buttonColor: Colors.blue, // 按钮颜色
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8), // 圆角半径
          ),
          textTheme: ButtonTextTheme.primary, // 按钮文本颜色
        ),
      ),
      home: HomePage(),
    );
  }
}

这些原生的设计资源看起来好像是够用的,但是当我们需要给 ListView 一个背景颜色时,没用合适字段去表示。而在大型的 Flutter 项目中这种情况加太多了,又该如何做呢?

定制自己的设计系统

设计系统是一套用于管理和共享设计资源的方法和工具集合。它包含了一系列的设计准则、组件、样式、布局规范等,将可重复使用的组件、样式指南(包括字体、颜色、尺寸等)和使用标准集合在一起集中去管理,旨在确保应用程序的视觉和交互一致性,并提高开发效率和设计协作能力。

对应到 App 中,将设计系统通常分为 3 类:

  • 原子级别:如 colorfontpaddingradius 等等,这是构成设计系统的基础。
  • 分子级别:如 buttoncheckboxesradio boxesdividersinput fields,是一些最基本和常见的 widget
  • 细胞级别:如 appbarscomplex card,甚至自定义 widget(如 CustomPainter),一些更复杂的 widget

下面来实现一套自己的设计系统。

Theme Extension

通常自定义一些常用的主题样式属性,会将其封装在 ThemeExtension 中,ThemeExtension 是对 ThemeData 类的扩展,用于简化主题样式的设置和访问,使用 ThemeExtension 轻松地定义和管理我们自己的主题样式,并在整个项目中访问和应用。

Color Theme

我们先以 Colors 为例,创建一个名为 AppColorsTheme 的类,继承自 ThemeExtension,如下面代码:

import 'package:flutter/material.dart';

class AppColorsTheme extends ThemeExtension<AppColorsTheme{

  static const _colorB0B0B0 = Color(0xFFB0B0B0);
  static const _colorEFEFEF = Color(0xFFEFEFEF);
  static const _color333333 = Color(0xFF333333);

  static const _color6C6C6C = Color(0xFF6C6C6C);
  static const _color7D7D7D = Color(0xFF7d7d7d);
  static const _color676767 = Color(0xFF676767);
  
  // 页面中真正使用的颜色名称
  final Color backgroundDefault;
  final Color backgroundInput;
  final Color textDefault;

  // 私有的构造函数
  const AppColorsTheme._internal({
    required this.backgroundDefault,
    required this.backgroundInput,
    required this.textDefault,
  });

  // 浅色主题工厂方法
  factory AppColorsTheme.light() {
    return const AppColorsTheme._internal(
        backgroundDefault: _colorB0B0B0,
        backgroundInput: _colorEFEFEF,
        textDefault: _color333333);
  }

  // 暗色主题工厂方法
  factory AppColorsTheme.dark() {
    return const AppColorsTheme._internal(
        backgroundDefault: _color6C6C6C,
        backgroundInput: _color7D7D7D,
        textDefault: _color676767);
  }

  @override
  ThemeExtension<AppColorsTheme> copyWith({bool? lightMode}) {
    if (lightMode == null || lightMode == true) {
      return AppColorsTheme.light();
    }
    return AppColorsTheme.dark();
  }

  @override
  ThemeExtension<AppColorsTheme> lerp(
          covariant ThemeExtension<AppColorsTheme>? other, double t) =>
      this;
}

这里将color name 与页面中实际使用的颜色变量名称分开,因为很多时候 color name 在不同的主题模式下是不一样的,虽然只提供了 lightdark,当然我还可以添加更多其它的主题颜色。

Text Theme

创建文本样式主题,命名为 AppTextsTheme,同样继承自 ThemeExtension

import 'package:flutter/material.dart';

class AppTextsTheme extends ThemeExtension<AppTextsTheme{
  static const _baseFamily = "Roboto";

  final TextStyle labelH1;
  final TextStyle labelH2;
  final TextStyle labelTextDefault;

  const AppTextsTheme._internal(
      {required this.labelH1,
      required this.labelH2,
      required this.labelTextDefault});

  factory AppTextsTheme.main() => const AppTextsTheme._internal(
      labelH1: TextStyle(
        fontFamily: _baseFamily,
        fontWeight: FontWeight.w400,
        fontSize: 18,
        height: 1.4,
      ),
      labelH2: TextStyle(
        fontFamily: _baseFamily,
        fontWeight: FontWeight.w300,
        fontSize: 16,
        height: 1.4,
      ),
      labelTextDefault: TextStyle(
        fontFamily: _baseFamily,
        fontWeight: FontWeight.w400,
        fontSize: 16,
        height: 1.2,
      ));

  @override
  ThemeExtension<AppTextsTheme> copyWith() {
    return AppTextsTheme._internal(
      labelH1: labelH1,
      labelH2: labelH2,
      labelTextDefault: labelTextDefault,
    );
  }

  @override
  ThemeExtension<AppTextsTheme> lerp(
          covariant ThemeExtension<AppTextsTheme>? other, double t) =>
      this;
}

这里的字体等文本样式需要和 UI 设计人员沟通好,而且最好命名也和他们设计稿的名称保持一致,这样在后期重复使用的时候能最大的降低沟通成本。

Dimension Theme

AppDimensionsTheme 主要存放项目中的尺寸相关的主题数据,例如间距、大小、边框宽度等。这些尺寸数据通常用于保持应用程序中的视觉一致性和布局稳定性。

import 'package:flutter/material.dart';

class AppDimensionsTheme extends ThemeExtension<AppDimensionsTheme{
  final double radiusCommitButton;
  final EdgeInsets paddingOrderList;

  const AppDimensionsTheme._internal({
    required this.radiusCommitButton,
    required this.paddingOrderList,
  });

  factory AppDimensionsTheme.main() => const AppDimensionsTheme._internal(
        radiusCommitButton: 8,
        paddingOrderList: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
      );

  @override
  ThemeExtension<AppDimensionsTheme> copyWith() {
    return AppDimensionsTheme._internal(
      radiusCommitButton: radiusCommitButton,
      paddingOrderList: paddingOrderList,
    );
  }

  @override
  ThemeExtension<AppDimensionsTheme> lerp(
          covariant ThemeExtension<AppDimensionsTheme>? other, double t) =>
      this;
}

这里一般是放一些比较通用的尺寸,而不是将所有的和尺寸相关的都放在这里。你可能会问,尺寸都写死在这里,那如何做响应式 UI 呢?

响应式 UI

FlutterView 创建一个 extension,命名为 FlutterViewExtension

import 'package:flutter/gestures.dart';

extension FlutterViewExtension on FlutterView {
  static const double responsive360 = 360;
  static const double responsive480 = 480;
  static const double responsive600 = 600;
  static const double responsive800 = 800;
  static const double responsive900 = 900;
  static const double responsive1200 = 1200;

  double get logicalWidth => physicalSize.width / devicePixelRatio;

  double get logicalHeight => physicalSize.height / devicePixelRatio;

  double get logicalWidthSA =>
      (physicalSize.width - padding.left - padding.right) / devicePixelRatio;

  double get logicalHeightSA =>
      (physicalSize.height - padding.top - padding.bottom) / devicePixelRatio;

  bool get isSmallSmartphone {
    if (logicalWidthSA < logicalHeightSA) {
      return (logicalWidthSA <= responsive360 ||
          logicalHeightSA <= responsive600);
    } else {
      return (logicalWidthSA <= responsive600 ||
          logicalHeightSA <= responsive360);
    }
  }

  bool get isRegularSmartphoneOrLess {
    if (logicalWidthSA < logicalHeightSA) {
      return (logicalWidthSA <= responsive480 ||
          logicalHeightSA <= responsive800);
    } else {
      return (logicalWidthSA <= responsive800 ||
          logicalHeightSA <= responsive480);
    }
  }

  bool get isSmallTabletOrLess {
    if (logicalWidthSA < logicalHeightSA) {
      return (logicalWidthSA <= responsive600 ||
          logicalHeightSA <= responsive900);
    } else {
      return (logicalWidthSA <= responsive900 ||
          logicalHeightSA <= responsive600);
    }
  }

  bool get isRegularTabletOrLess {
    if (logicalWidthSA < logicalHeightSA) {
      return (logicalWidthSA <= responsive800 ||
          logicalHeightSA <= responsive1200);
    } else {
      return (logicalWidthSA <= responsive1200 ||
          logicalHeightSA <= responsive800);
    }
  }
}

AppDimensionsTheme 中使用,只需要在这里加上判断就可以了。

factory AppDimensionsTheme.main(FlutterView flutterView) =>
  AppDimensionsTheme._internal(
    radiusCommitButton: flutterView.isSmallSmartphone ? 8 : 16,
    paddingOrderList:
        const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
  );

如何使用 Theme Extension

第一步:在 main.dart 文件,将以下代码放在程序入口 WidgetMaterialApp 下面。

MaterialApp(
  title: 'Flutter技术实践',
  theme: Theme.of(context).copyWith(
    extensions: [
      AppTextsTheme.main(),
      AppColorsTheme.light(),
      AppDimensionsTheme.main(View.of(context)),
    ],
  )
)

第二步:为 ThemeData 创建一个 extension,目的是简化了调用代码。

import 'package:flutter/material.dart';
import 'package:flutter_todo/resources/app_colors_theme.dart';
import 'package:flutter_todo/resources/app_dimensions_theme.dart';
import 'package:flutter_todo/resources/app_texts_theme.dart';

extension ThemeDataExtension on ThemeData {
  AppDimensionsTheme get appDimensions => extension<AppDimensionsTheme>()!;

  AppColorsTheme get appColors => extension<AppColorsTheme>()!;

  AppTextsTheme get appTexts => extension<AppTextsTheme>()!;
}

第三步:开始调用,这里显示文本样式为 labelTextDefault,颜色为 textDefault

Text(
  "Flutter",
  style: Theme.of(context).appTexts.labelTextDefault.copyWith(
    color: Theme.of(context).appColors.textDefault,
  ),
),

分层架构中去管理设计系统

架构分层

从上面的分层设计架构图可以到,把与 Theme Extension 相关的划分到 resources 的组件包(在上图红框内),resources 组件包的目录结构如下:

resources 组件包目录结构

resources 的组件包在整个架构中是作为基础组件包,不需要依赖任何其它组件包,这样在使用 ThemeExtension 时可以确保整个应用程序中使用的主题样式保持一致,从而提高用户体验和视觉一致性。

小结

本篇介绍了 Flutter 大型项目分层架构中的UI设计系统实现,主要是原子级别的,如 colorfontpaddingradius 等等设计系统的基础,下一篇来介绍设计系统中分子级别和细胞级别,感谢您的阅读,记得关注加点赞哦!