0%

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

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

Flutter 默认的设计系统

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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,如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 主要存放项目中的尺寸相关的主题数据,例如间距、大小、边框宽度等。这些尺寸数据通常用于保持应用程序中的视觉一致性和布局稳定性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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 中使用,只需要在这里加上判断就可以了。

1
2
3
4
5
6
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 下面。

1
2
3
4
5
6
7
8
9
10
MaterialApp(
title: 'Flutter技术实践',
theme: Theme.of(context).copyWith(
extensions: [
AppTextsTheme.main(),
AppColorsTheme.light(),
AppDimensionsTheme.main(View.of(context)),
],
)
)

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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
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 等等设计系统的基础,下一篇来介绍设计系统中分子级别和细胞级别,感谢您的阅读,更多该系列干货文章请关注我关注号:Flutter技术实践,记得关注加点赞哦!

Flutter技术实践