Flutter 脚手架
阅读 Flutter 开发文档之后,整理了一份开发
Flutter
项目涉及到的知识点,如目录结构、项目分层、路由、序列化及常用的库。把它们集成到一个脚手架中,便于知识的沉淀和项目的复用,这也是写这篇文章的目的。
架构
分层架构
在计算机领域中,分层是解决复杂问题常用的方法,在
Flutter中也可以沿用这一个概念,它将应用程序组织成不同的层,每一层都有特定的角色和职责。通常,应用程序根据复杂程度分为
2 到 3 层。 ![]()
- UI 层 - 显示业务逻辑层暴露给用户的数据,并处理用户交互。这通常也称为“表示层”。
- 逻辑层 - 实现核心业务逻辑,并促进数据层和 UI 层之间的交互。通常被称为“领域层”。逻辑层是可选的,只有当您的应用程序具有在客户端上发生的复杂业务逻辑时才需要实现它。许多应用程序只关心向用户呈现数据并允许用户更改该数据(通俗地称为 CRUD 应用程序)。这些应用程序可能不需要这个可选层。
- 数据层 - 管理与数据源的交互,例如数据库或平台插件。向业务逻辑层公开数据和方法。
层与层之间遵循一个原则,就是每一层只能与直接上下层的层进行通信。UI 层不知道数据层是否存在,反之亦然。
单一数据源 (Single source of truth)
单一数据源是指每种类型的数据只会有一个数据源,即将同一类型的数据源封装在一个类中,这也是程序高内聚的表现。通常,应用程序中任何给定类型数据的源头都保存在一个名为
Repository (仓库)
的类中,该类是数据层的一部分,对于应用程序中的每种类型的数据,都有一个仓库类,同时,一个
Repository (仓库)
只包含一个类型的数据,如在一个股票程序中,认证类型中的
Repository 类中只能包含认证相关的数据,不能包含
股票 类型的数据,股票
类型的数据应该包含在股票类型的 Repository 类中。
这个原则也可以应用于应用程序中的各个层和组件,以及单个类中。
单向数据流
单向数据流 (UDF)
指的是一种设计模式,有助于将状态与显示该状态的 UI
解耦。最简单地说,状态从数据层流经逻辑层,最终流向 UI 层的
widgets。来自用户交互的事件反向流动,从表示层流回逻辑层,再到数据层。

在 UDF 中,从用户交互到重新渲染 UI 的更新循环如下:
- 【UI 层】 由于用户交互(例如单击按钮)而发生事件。小部件的事件处理程序回调调用逻辑层中的类公开的方法。
- 【逻辑层】 逻辑类调用仓库公开的方法,这些方法知道如何修改数据。
- 【数据层】 仓库更新数据(如果需要),然后将新数据提供给逻辑类。
- 【逻辑层】逻辑类保存其新状态,并将其发送到 UI。
- 【UI 层】UI 显示视图模型的新状态。
UI 是 (不可变) 状态的函数
Flutter 是声明式的,这意味着 UI
反映应用程序的当前状态。当状态更改时,应用程序应该触发依赖于该状态的
UI的重建。在 Flutter 中,这种描述被称为 “UI
是状态的函数”。 
在这种模式中,是数据驱动
UI,而不是相反。数据应该是不可变的和持久的,并且视图应该包含尽可能少的逻辑。这最大限度地减少了应用程序关闭时数据丢失的可能性,并使得应用程序更易于测试且不易出错。
MVVM
在上节提到的分层架构中,每个层级进一步划分为不同的组件,每个组件都有明确的职责、定义良好的接口、边界和依赖关系。在
Flutter 中, 使用模型-视图-视图模型 (MVVM)
的架构模式,它将应用程序分离为三个部分的组件,分别是:模型、视图模型和视图。视图和视图模型构成了应用程序的
UI 层。仓库和服务代表应用程序的数据,或者 MVVM 的模型层。

应用程序中的每个功能都将包含一个视图来描述 UI
和一个视图模型来处理逻辑,一个或多个仓库作为应用程序数据的真实来源,以及零个或多个与外部
API(如客户端服务器)交互的服务。 
具有复杂逻辑的应用可能还具有位于 UI 层和数据层之间的逻辑层。该逻辑层通常称为领域层。领域层包含其他组件,通常称为交互器或用例。
UI 层
应用程序的 UI 层负责与用户交互。它向用户显示应用程序的数据,并接收用户输入,例如点击事件和表单输入。 UI 响应数据更改或用户输入。当 UI 从仓库接收到新数据时,它应该重新渲染以显示该新数据。当用户与 UI 交互时,它应该更改以反映该交互。
UI 层由两个架构组件组成,基于 MVVM 设计模式:
- 视图View描述了如何向用户呈现应用程序数据。具体来说,它们指的是构成特性的小部件组合。例如,视图通常(但不总是)是一个具有
Scaffold小部件的屏幕,以及小部件树中下面的所有小部件。视图还负责将事件传递给视图模型以响应用户交互。 - 视图模型ViewModel包含将应用程序数据转换为
UI 状态的逻辑,因为仓库中的数据通常与需要显示的数据格式不同。例如,您可能需要组合来自多个仓库的数据,或者您可能想要过滤数据记录列表。
视图和视图模型应该具有一对一的关系。最简单地说,视图模型管理 UI 状态,视图显示该状态。
视图(View)
在 Flutter 中,视图是应用程序的小部件类。视图是渲染 UI 的主要方法,不应包含任何业务逻辑。它们应该从视图模型传递所有需要渲染的数据。
视图应包含的功能有:
- 简单的 if 语句,根据视图模型中的标志或可为空字段显示和隐藏小部件
- 动画逻辑
- 基于设备信息(如屏幕尺寸或方向)的布局逻辑。
- 简单的路由逻辑
所有与数据相关的逻辑应在视图模型中处理。
视图模型(ViewModel)
视图模型公开渲染视图所需的应用程序数据,它主要的职责包括:
- 从仓库检索应用程序数据并将其转换为适合在视图中呈现的格式。例如,它可以过滤、排序或聚合数据。
- 维护视图中所需当前状态,以便视图可以在不丢失数据的情况下重建。例如,它可能包含布尔标志以有条件地在视图中渲染小部件,或者跟踪屏幕上活动的轮播部分。
- 向视图公开回调(称为命令),这些回调可以附加到事件处理程序,例如按钮按下或表单提交。
命令以 命令模式 命名,是 Dart
函数,允许视图在不知道其实现的情况下执行复杂逻辑。命令作为视图模型类中的成员编写,由视图类中的手势处理程序调用。
数据层
应用程序的数据层处理您的业务数据和逻辑。两个架构组成部分构成了数据层:服务和仓库。
仓库(Repository)
仓库类是模型数据的真实来源。它们负责从服务轮询数据,并将原始数据转换为领域模型。领域模型表示应用程序所需的数据,以视图模型类可以使用的格式格式化。您的应用中处理的每种不同类型的数据都应该有一个仓库类。
仓库处理与服务相关的业务逻辑:
- 缓存
- 错误处理
- 重试逻辑
- 刷新数据
- 轮询服务以获取新数据
- 根据用户操作刷新数据
仓库输出的模型由视图模型使用。仓库和视图模型之间存在多对多的关系。一个视图模型可以使用多个仓库来获取所需的数据,并且一个仓库可以被多个视图模型使用。
服务(Service)
服务是应用程序的最低层级。它们封装 API 端点并公开异步响应对象,例如 Future 和 Stream 对象。它们仅用于隔离数据加载,并且不保存任何状态。您的应用应该为每个数据源有一个服务类。服务可能封装的端点示例包括:
- 底层平台,例如 iOS 和 Android API
- REST 端点
- 本地文件
服务和仓库之间存在多对多的关系。单个仓库可以使用多个服务,并且一个服务可以被多个仓库使用。
可选:领域层
随着应用程序的增长和添加功能,您可能需要抽象化添加到视图模型中过多复杂性的逻辑。这些类通常称为交互器或用例。
用例负责使 UI
和数据层之间的交互更简单、更可重用。它们从仓库中获取数据,并使其适合 UI
层使用。 
用例主要用于封装原本应该存在于视图模型中的业务逻辑,并且满足以下一个或多个条件:
- 需要合并来自多个仓库的数据
- 极其复杂
- 该逻辑将由不同的视图模型重用
该层是可选的,因为并非所有应用程序或应用程序中的功能都需要这些要求。
目录结构
脚手架的目录结构使用两种方式来组织,分别是按照功能和按照类型(按照层次),目录的主体结构首先按照类型来划分,如 ui、data、domain、routing 和 utils 等等,同时在一个大的目录下,又按照功能划分为不同的子目录,如在 ui目录下按照功能切分为 auth、home 和 setting 等目录。目录的整体结构如下:
- ui - 存放所有视图相关的代码,按照不同的功能进行划分,一个功能下又分为 view_models 和 widgets,存放相关联的视图模型和视图。
- data - 存放数据层的代码,包括仓库和服务。
- domain - 存放领域对象,即工程中用到的业务对象或UI状态对象,可以按照功能进行划分。
- routing - 存放路由相关的代码,如 go_route相关的数据。
- utils - 存放工具类。
- config - 存放配置相关的代码。
- test - 包含测试代码,其自身结构与
lib匹配。 - testing - 包含可以在其他包的测试代码中使用的模拟对象和其他测试实用程序。
一个功能模块可以分拆到 ui、data 和 domain不同的目录中,建议使用相同的功能模块来命名,如认证模块 auth, 在 ui、data 和 domain 目录中都统一使用 auth 目录来存放其相关联的代码。
1 | lib |
关键技术点
在该脚手架中,使用了如下的技术点:
- 使用 MVVM 模式编码;
- 使用 命令模式 来安全地渲染数据更改时的 UI;
- 使用 ChangeNotifier 和 Listenable 对象来管理状态;
- 使用 package:provider 实现依赖注入;
- 使用 go_route 实现页面路由;
- 使用 json_serializable 序列化数据;
- 使用 flutter_localizations 国际化文本;
- 使用 test 及 flutter_test 进行单元测试。
MVVM 模式编码
MVVM 模式就是用 MVVM 分层结构来拆分组织代码:
- 使用service层来封装对外的接口请求;
- 使用repository层来包装 servic 接口,加上缓存、重试等机制;
- 使用viewmode层调用repository层获取数据,并向视图层提供状态管理能力,触发view的渲染;
- 使用view来监听viewmode层的状态变化,最终实现view的渲染。
MVVM 模式本质上就是基于状态来编程,以 auth 认证模块为例。
服务Serice层
认证模块需要调用外部服务,如通过 Restful 接口验证用户密码获取Token、刷新 Token和退出等操作,这些调用外部服务的操作就可以封装在 service 层中。
1 | class ApiService { |
getToken 是一个异步方法,封装了 restful
的认证接口,以便 repository 层调用。
仓库Repository层
Repository层在服务Serice层基础上,可以加入一些高级的功能,如缓存和重试机制,在这里,简单起见,只是做了一个转发,没有加入额外的功能。
1 | class AuthRepositoryRemote extends AuthRepository { |
视图模型VidwModel层
视图模型VidwModel和视图View是基于事件通知的机制来联动,视图模型VidwModel实现
ChangeNotifier
接口,作为事件发起方,管理的状态发生改变时通过
notifyListeners() 方法通知视图view 进行渲染。视图View通过
Listenable 对象来监听状态的的变化,从而进行渲染。
在正常情况下,登录结果一般会用三种状态:登录中,成功登录和失败,视图模型会维护这三个状态,视图view会根据这三个状态进行不同界面的展示。
1 | class LoginViewModel with ChangeNotifier { |
视图View
视图view会根据状态的不同,显示不同的界面,如在下面的代码中,如果状态为登录中,ElevatedButton
按钮会显示一个加载中的动画否则就显示 “登录”字样。
1 | class LoginScreen extends StatefulWidget { |
命令模式
在上一节中,在视图模型VidwModel层中,一个登录操作涉及到一个登录方法和三个状态(登录中、登录成功和登录失败),一般情况下一个VidwModel中会有多个操作,如除了登录,还有刷新和退出操作,每一个操作都会有一个方法和多个状态,如刷新也会有刷新成功和失败。这时候,如何管理新加入的状态呢?一种方法是有几个状态就加入几个状态,另外一种方式是在各个操作中复用状态,如成功和失败可以复用。这两种方式都有缺点,第一种会造成状态的繁多,不利于代码的复用;第二种则加大了状态管理的复杂度,容易出错。有没有更好的方式呢?
命令模式优美地解决上述的问题,它本质上将一个操作封装成一个命令对象,在这个对象中,包含执行的方法和状态。如登录命令对象中,包含了一个登录方法和登录后的状态。命令对象本身实现了
ChangeNotifier 接口,可以单独完成状态的通知。
一个命令对象如下所示:
- _action - 是一个函数对象,代表操作执行的方法;
- 状态 - 封装了三个状态:running, error 和 completed.
1 | class Command extends ChangeNotifier { |
状态管理
Flutter 是一个声明式的 UI 框架,开发人员描述当前 UI
的状态,状态变化之后框架会自动渲染
UI。如用户在设置屏幕中切换开关,改变了状态,这会触发用户界面的重绘。

状态的种类
UI 状态分为两类:临时状态和应用状态,临时状态(有时称为UI状态或局部状态)是指可以地包含在单个 Widget 中的状态,而应用状态是在多个 Widget 中共享的状态。
如下的状态是临时状态,它只在一个 Widget上使用:
- PageView中的当前页面;
- 复杂动画的当前进度;
- BottomNavigationBar 中当前选定的标签。
应用程序状态包括如下的状态:
- 用户偏好设置;
- 登录信息;
- 社交网络应用程序中的通知;
- 电子商务应用程序中的购物车;
- 新闻应用程序中文章的已读/未读状态。
可以如下方式判断一个状态是临时状态还是应用程序状态: 
临时状态
对于临时状态而言,不需要复杂的状态管理技术,只需要一个
StatefulWidget 即可。
在下面的例子中,底部导航栏中当前选定的项目保存在 _MyHomepageState
类的 _index 字段中,_index 是临时状态。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class MyHomepage extends StatefulWidget {
const MyHomepage({super.key});
State<MyHomepage> createState() => _MyHomepageState();
}
class _MyHomepageState extends State<MyHomepage> {
int _index = 0;
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: _index,
onTap: (newIndex) {
setState(() {
_index = newIndex;
});
},
// ... items ...
);
}
}
在这里,使用 setState()
来更新状态,从而达到切换页面的效果。
应用程序状态
应用程序状态在多个 Widget 中共享数据,对于它们的管理,需要借助相关技术来完成状态的管理,一般而言,可以使用官方的 provider 包,也可以使用第三方的包,如 Redux、Rx、hooks。
对于中小型的项目而言,provider 包足以胜任相关的工作。provider
包利用了 Flutter
内部数据共享的机制,即让组件将其数据和服务提供给它们的后代(所有后代),这个机制通过继承特殊类型的组件来实现,如
InheritedWidget、InheritedNotifier、InheritedModel
等等。
使用provider包
在使用 provider 之前,请将其依赖项添加到你的
pubspec.yaml 中。通过如下命令下载该包:
1 | flutter pub add provider |
然后便可以在代码中引入该包了:import 'package:provider/provider.dart';
使用 provider,要理解 3 个概念:
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
在 provider 中,ChangeNotifier
是封装应用状态的一种方式,它可以为状态提供变更通知功能。对于非常简单的应用,可以使用单个
ChangeNotifier。在复杂的应用中,有多个模型,就对应有多个
ChangeNotifiers。
以下面的购物车应用为例,ChangeNotifier
中管理购物车状态。
1 | class CartModel extends ChangeNotifier { |
当状态有变更时,通过 notifyListeners() 方法通知。
ChangeNotifierProvider
ChangeNotifierProvider 的作用是向其后代提供
ChangeNotifier 实例的组件,其后代可以直接使用
ChangeNotifier。
一般而言,ChangeNotifierProvider 位于
Widget 的最顶层。
1 | void main() { |
ChangeNotifierProvider 本质上是将
ChangeNotifier(CartModel)
类注入到应用程序中,如果想提供多个类,可以使用 MultiProvider。
1 | void main() { |
Provider 提供了将其它类型注入到程序中的能力,在后续的
依赖注入 章节中,便是使用它实现了 Service类 和
Repository类的注入功能。
Consumer
将 ChangeNotifier
注入到应用程序中之后,便可以消费和引用该类。有两种方式使用ChangeNotifier对象,一种是在
UI 界面中通过 Consumer 引用该对象,另外一种是在代码中通过
Provider.of 获取到该对象的引用,便可调用其方法。
通过 Consumer<CartModel> 消费
ChangeNotifier(CartModel)对象,在代码中,通过
cart 引用其实例对象,获取状态的值。
1 | return Consumer<CartModel>( |
通过 Provider.of 也可以直接获取
ChangeNotifier(CartModel) 对象,代码如下所示:
1 | Provider.of<CartModel>(context, listen: false).removeAll(); |
ListenableBuilder
除了使用 Consumer<CartModel> 来消费
ChangeNotifier(CartModel) 对象之外,也可以使用
ListenableBuilder类。相对于
Consumer,ListenableBuilder 需要手动指定
listenable 对象,如下所示: 1
2
3
4
5
6
7
8
9
10
11
12
13
14return ListenableBuilder(
listenable: widget.viewModel.login,
builder: (context, _) {
return FilledButton(
onPressed: () {
widget.viewModel.login.execute((
_email.value.text,
_password.value.text,
));
},
child: Text(AppLocalization.of(context).login),
);
},
);
依赖注入
依赖注入 主要是解决对象的创建和引用的问题。在
MVVM
分层模式中,处于上层的对象会依赖下层的对象,这种依赖主要是在层对象中持有层对象的引用,这些引用一般通过构造函数中传入,如将
Service 传递到 Repository 中。
1
2
3
4
5
6class MyRepository {
MyRepository({required MyService myService})
: _myService = myService;
late final MyService _myService;
}
在该脚手架中,使用 package:provider 来处理这个问题。服务
Service、仓库 Ropository 和
ChangeNotifier 作为 Provider 对象暴露给
Flutter 应用程序的 widget 树的顶层,再通过
provider 中的 BuildContext.read
方法注入到使用的对象中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25void main() {
WidgetsFlutterBinding.ensureInitialized();
AppLogger.init(level: Level.debug);
runApp(
MultiProvider(
// 同时提供多个ViewModel
providers: [
Provider(create: (context) => ApiService()),
Provider(create: (context) => SharedPrefsService()),
Provider(
create: (context) =>
AuthRepositoryRemote(apiService: context.read())
as AuthRepository,
),
ChangeNotifierProvider(
create: (context) => LoginViewModel(
authRepository: context.read(),
sharedPrefsService: context.read(),
),
),
],
child: MyApp(),
),
);
}
将
Service,Repository和ChangeNotifier
添加到应用程序之后,可以在 View 对象中通过
context.read() 引入它们。
1 | GoRouter router(AuthRepository authRepository) => GoRouter( |
路由
go_router 是由 Flutter
团队成员开发的声明式路由库,相比 Navigator 1.0/2.0
更简洁,支持深层链接、路由守卫等功能。
在这里,便使用go_router 完成路由功能,预先定义好
url链接。 1
2
3
4
5
6
7
8
9
10
11
12
13
14abstract final class Routes {
static const splash = '/splash';
static const home = '/home';
static const login = '/login';
static const search = '/$searchRelative';
static const searchRelative = 'search';
static const results = '/$resultsRelative';
static const resultsRelative = 'results';
static const activities = '/$activitiesRelative';
static const activitiesRelative = 'activities';
static const booking = '/$bookingRelative';
static const bookingRelative = 'booking';
static String bookingWithId(int id) => '$booking/$id';
}
定义好 url链接与视图 View 对应关系。
1 | GoRouter router(AuthRepository authRepository) => GoRouter( |
最后加入路由守卫功能。
1 | Future<String?> _redirect(BuildContext context, GoRouterState state) async { |
序列化
序列化是指领域对象(业务对象)与 json
字符串之间的相互转换,用的库是
json_serializable。使用它非常简单,只要定义好领域对象的属性,再配合相应的注解就可以了,转换的代码通过代码生成器自动生成。
首先引入依赖: 1
2
3
4
5
6
7
8
9
10
11# pubspec.yaml
dependencies:
flutter:
sdk: flutter
json_annotation: ^4.8.1 # JSON注解库
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.6 # 代码生成工具
json_serializable: ^6.7.1 # JSON序列化代码生成
接着创建类:
1 | import 'package:json_annotation/json_annotation.dart'; |
_$AuthTokenFromJson 与 _$AuthTokenToJson
这两个方法有特殊的命名方式,前缀要加上领域对象的类名,由代码生成器生成。
最后生成序列化的代码:
1 | # 一次性生成代码 |
运行后会生成 auth_token.g.dart 文件:
1 | part of 'auth_token.dart'; |
国际化
Flutter 的国际化依赖于下面几个核心组件:
- MaterialApp/ CupertinoApp/ WidgetsApp:应用的根组件。它们通过 localizationsDelegates和 supportedLocales这两个参数来配置国际化。
- Localizations:一个 Inherited Widget,位于 Widget 树的顶层(通常由
MaterialApp创建)。它承载了当前语言环境的本地化资源,并允许树中任何子
Widget 方便地获取这些资源。
- Locale:一个表示语言和地区信息的类。例如:
- Locale(‘en’, ‘US’)表示美国英语;
- Locale(‘zh’, ‘CN’)表示中国大陆简体中文;
- Locale(‘zh’, ‘TW’)表示台湾繁体中文。
- LocalizationsDelegate:这是核心中的核心。它是一个委托类,负责在应用运行时,为特定的 Locale异步加载对应的本地化资源集合。
LocalizationsDelegate 详解
LocalizationsDelegate<T>是一个抽象类,它为每一种本地化资源(例如,应用自己的字符串
或者 Material 组件库的文本)创建一个它的实现。其中
<T>是资源类的类型。
它的核心职责和生命周期方法:
- bool isSupported(Locale locale)
- 作用:判断当前 delegate 是否支持某个特定的 Locale。
- 示例:你的应用只支持英文和中文,那么当系统切换到法语时,这个方法应该对法语返回 false。Flutter 会尝试寻找一个支持的语言(通过 supportedLocales的回退机制)。
- Future
load(Locale locale) - 作用:异步加载指定 Locale对应的本地化资源对象(T类型的实例)。
- 这是实际加载翻译文件(如 JSON、YAML、arb
文件)或硬编码映射表的地方。返回一个 Future
。
- bool shouldReload(covariant LocalizationsDelegate
old) - 作用:当 App 的 Localizations组件被重建时,决定是否需要重新调用 load方法来加载资源。
- 通常通过比较新旧 delegate 的某些属性来判断。如果资源是静态的,通常返回 false。如果支持动态切换语言,可能需要返回 true或根据版本号判断。
在脚手架中,LocalizationsDelegate 定义如下:
1 | // Copyright 2024 The Flutter team. All rights reserved. |
出于简化的目的,翻译的文本使用一个 Map
对象来存储,此外,也可以使用 JSON、YAML、arb 文件来存储。
接下来在 MaterialApp 中注册代理对象和使用国际化文本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp.router(
// 注册本地化代理
localizationsDelegates: [
GlobalWidgetsLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
AppLocalizationDelegate(),
],
// 引用国际化文本
title: AppLocalization.of(context).appTitle,
theme: ThemeData(primarySwatch: Colors.blue),
themeMode: ThemeMode.system,
routerConfig: router(context.read()),
);
}
}
最后,使用国际化需要引入如下依赖包: 1
2
3
4dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.20.2
测试
测试一般分为如下几类:
- 单元测试:测试单个函数、方法或类;
- 组件测试:测试单个组件;
- 集成测试:测试完整的应用或应用的大部分。
以单元测试为例,需要先引入 test
库,如果需要模拟第三方的依赖,还需要引入
Mockito 包,下面以一个简单的例子为例。创建两个文件:counter.dart
和 counter_test.dart。counter.dart
文件包含您想要测试的类,并位于 lib 文件夹中。
counter_test.dart 文件包含测试本身,并位于
test 文件夹中。
通常,测试文件应位于 Flutter 应用或包根目录下的
test 文件夹中。 测试文件应始终以 _test.dart
结尾,这是测试运行器在搜索测试时使用的约定。
完成之后,文件夹结构应如下所示: 1
2
3
4
5counter_app/
lib/
counter.dart
test/
counter_test.dart
首先创建要测试的 counter.dart,
主要功能是实现递增和递减。
1 | class Counter { |
接下来编写测试代码,在 counter_test.dart
文件中,编写第一个单元测试。 测试使用顶级
test函数定义,您可以使用顶级 expect
函数来检查结果是否正确。 这两个函数都来自 test 包。
1 | // Import the test package and Counter class |
如果想运行一系列相关的测试,可以使用 flutter_test 包的
group 函数来对测试进行分类。
放入组后,可以使用一个命令对该组中的所有测试调用
flutter test。
1 | import 'package:counter_app/counter.dart'; |
使用单元测试需要引入如下的包或库: 1
2
3
4dev_dependencies:
flutter_test:
sdk: flutter
test: ^1.24.0
组件测试和集成测试需要另外库,如需要进一步了解,可以查看官方文档。
其它
其它知识点待续 ...
参考: