dart 语言学习日记(4)
dart 语言学习日记(4)
前言
上一个章节我们讲了如何构建 ui 以及如何正确处理事件,而如果需要构建一个完整的 flutter 应用,后端的设计是必不可少的\
这一章节,结合实际的项目,我们来讲一讲如何构建一个 flutter 应用后端
本博客所需要讲明的内容
- 如何从0开始构建一个 flutter 应用后端
主要内容
构建一个 flutter 应用
> cd ~/Study/flutterStudy/
> flutter create domitory_manager
> cd domitory_manager
> flutter run
如上文所示,我们就已经构建了一个 flutter 应用
理论准备
在 flutter 中,构建一个后端应用,可以模仿 java spring-boot 应用的工程目录
我们新建 daos / models / repositories 三个文件夹,分别用于存储如下内容:
daos该文件夹主要用于存放 数据访问对象类 (DAO,Data Access Object),该类主要负责与数据库进行交互models该文件夹主要用于存放 数据模型类 该类定义了 flutter 应用程序中使用的数据结构以及对象repositories该文件夹主要用于存放业务逻辑,其负责与 BLOC 的交互
根据以上准备,我们的工程目录应该是这样的
> cd domitory_manager
> tree -L1 ./lib
./lib
├── daos
├── main.dart
├── models
├── repositories
├── ui
安装并初始化 sqlite3
在一个简易的 flutter 应用中,我倾向于使用 sqlite3 作为应用的 数据持久层 的具体实现
// 安装 sqlite 客户端
> yay -Ss sqlite3
> yay -S sqlite
> cd domitory_manager
// 创建 assets 文件夹用于存放静态文件
> mkdir assets
// 新的工程目录如下
> tree -L1
.
├── analysis_options.yaml
├── android
├── assets
├── build
├── devtools_options.yaml
├── dist
├── distribute_options.yaml
├── domitory_manager.iml
├── ios
├── lib
├── linux
├── macos
├── pubspec.lock
├── pubspec.yaml
├── README.md
├── test
├── web
└── windows
12 directories, 7 files
-
> cd aassets
// 创建名为 `domitory_manager.db` 的数据库
> sqlite3 domitory_manager.db
// 创建 `sql` 文件夹用于存放模拟数据以及数据库初始化文件
> mkdir sql
> cd sql
// 创建 `init.sql` 文件用于初始化数据库
> cat > init.sql <<EOF
CREATE TABLE IF NOT EXISTS UNIT(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS USER(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
unit_id INTEGER NOT NULL,
treatment INTEGER NOT NULL,
FOREIGN KEY (unit_id) REFERENCES UNIT(id)
);
CREATE TABLE IF NOT EXISTS ROOM(
room_number INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE,
status TEXT CHECK(status IN ('live', 'vacate', 'off_live')) NOT NULL DEFAULT 'off_live',
since DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES USER(id)
);%
EOF
// 回退到 `assets` 文件夹下
> cd ..
// 初始化 sqlite3 数据库
> sqlite3 domitory_manager.db < ./sql/init.sql
通过以上步骤,我们就已经初始化了一个 sqlite3 数据库了
现在我们来查看一下数据库里的相关表格
> cd domitory_manager
> sqlite3 ./assets/domitory_manager.db
SQLite version 3.50.0 2025-05-29 14:26:00
Enter ".help" for usage hints.
sqlite> .tables
ROOM UNIT USER
sqlite> .schema ROOM
CREATE TABLE ROOM(
room_number INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE,
status TEXT CHECK(status IN ('live', 'vacate', 'off_live')) NOT NULL DEFAULT 'off_live',
since DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES USER(id)
);
sqlite>.quit
在上文中,我们总共使用了三个 sqlite3 命令
.tables用于查看 sqlite3 中的所以数据表.schema ${表名}用于查看指定表的表结构.quit用于退出 sqlite3 客户端
我基本上常用的就以上三个命令,如果有更多的需求可以在 sqlite3 客户端中输入 .help 查看更多功能及命令
根据以上操作,我们已经成功安装并初始化一个 sqlite3 数据库了
引入 sqlite3 依赖
参考资料:
Flutter pubspec options 依赖引入文档
Flutter 中国区配置方法
在工程根目录下编辑 pubspec.yaml 文件
name: domitory_manager
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.7.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
flutter_localizations: # 添加 flutter_localizations 依赖
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
sqflite: ^2.4.2
path: ^1.9.1
path_provider: ^2.1.5
sqflite_common_ffi: ^2.3.5
flutter_bloc: ^9.1.0
mockito: ^5.4.5
equatable: ^2.0.7
intl: ^0.20.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- assets/domitory.db
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
上文的内容大部分都是 flutter create 生成的,主要的改动在 dependencies 与 assets 中, 进行一定的参考即可
在调整好 pubspec.yaml 的内容后,敲入 flutter pub get 即可下载相关的依赖,如果依赖下载有问题参考本章开头的参考资料配置
至此,我们已经引入包括 sqlite3 在内的大部分重要依赖
构建层级代码
解决了依赖问题后需要在理论准备的基础上编写相关代码
理论准备 的各层级关系如下
从底往上,先定义 models 再定义 daos 最后定义 repositories
- models 层定义
# unit.dart
class Unit {
final int? id; // 将 id 设置为可选
final String name;
Unit({this.id, required this.name});
factory Unit.fromMap(Map<String, dynamic> map) {
return Unit(
id: map['id'], // id 可能为 null
name: map['name'],
);
}
Map<String, dynamic> toMap() {
final map = {'name': name}; // 插入时不包含 id
if (id != null) {
map['id'] = id!.toString(); // 更新时包含 id
}
return map;
}
}
参考以上代码,即可定义各数据类型
在这个基础之上定义一个联合类型
#user_room.dart
import './user.dart';
import './room.dart';
class UserRoom {
final Room room;
final User? user;
UserRoom({required this.room, required this.user});
}
- 定义 daos 层
该层用于定义与数据库的交互逻辑
示例代码如下
import 'package:domitory_manager/daos/database_initializer.dart';
import 'package:sqflite/sqflite.dart';
import '../models/unit.dart';
class UnitDao {
final DatabaseInitializer _databaseInitializer;
UnitDao(this._databaseInitializer);
Future<List<Unit>> getUnits() async {
final Database db = await _databaseInitializer.database;
// 读取 sqlite3 数据库里 unit 表的所有数据
final List<Map<String, dynamic>> maps = await db.query('unit');
return List.generate(maps.length, (i) => Unit.fromMap(maps[i]));
}
Future<Unit?> getUnitById(int id) async {
final Database db = await _databaseInitializer.database;
// 这个写法等价于 select * from unit where id = ?
final List<Map<String, dynamic>> maps = await db.query(
'unit',
where: 'id =?',
whereArgs: [id],
);
return maps.isNotEmpty ? Unit.fromMap(maps[0]) : null;
}
Future<int> insertUnit(Unit unit) async {
final Database db = await _databaseInitializer.database;
// 这个写法等价于 insert into unit (id,name) values (?,?)
// 这个 tomap 就是用于解耦 (?,?)
return await db.insert('unit', unit.toMap()); // toMap() 不包含 id
}
Future<int> updateUnit(Unit unit) async {
final Database db = await _databaseInitializer.database;
return await db.update(
'unit',
unit.toMap(),
where: 'id = ?',
whereArgs: [unit.id],
);
}
Future<int> deleteUnit(int id) async {
final Database db = await _databaseInitializer.database;
return await db.delete('unit', where: 'id = ?', whereArgs: [id]);
}
Future<Unit?> getUnitByName(String name) async {
final Database db = await _databaseInitializer.database;
final List<Map<String, dynamic>> maps = await db.query(
'unit',
where: 'name = ?',
whereArgs: [name],
limit: 1,
);
if (maps.isNotEmpty) {
return Unit.fromMap(maps.first);
}
return null;
}
}
再定义一个联合类型
#user_room_dao.dart
import 'package:domitory_manager/models/user.dart';
import './database_initializer.dart';
import '../models/user_room.dart';
import '../models/room.dart';
class UserRoomDao {
final DatabaseInitializer _databaseInitializer;
UserRoomDao(this._databaseInitializer);
Future<List<UserRoom>> getRoomsByUnit(String unit) async {
final db = await _databaseInitializer.database;
// 使用 rawQuery 代替 db 用法
final List<Map<String, dynamic>> results = await db.rawQuery(
'''SELECT r.*,u.*
FROM room r
LEFT JOIN user u ON r.user_id = u.id
LEFT JOIN UNIT unit ON u.unit_id = unit.id
WHERE unit.name = ?''',
[unit],
);
return results.map((row) {
final room = Room(
roomNumber: row['room_number'],
// status 属于枚举类型,需要这样匹配出枚举类型
status: RoomStatus.values.firstWhere(
(e) => e.toString().split('.').last == row['status'],
),
since: row['since'] != null ? DateTime.parse(row['since']) : null,
);
final user =
row['u.id'] != null
? User(
id: row['u.id'],
name: row['u.name'],
unitId: row['u.unit_id'],
treatment: row['u.treatment'],
)
: null;
return UserRoom(room: room, user: user);
}).toList();
}
}
接下来比较重要的是定义数据库初始化类
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class DatabaseInitializer {
static Database? _database;
final String databasePath;
DatabaseInitializer({required this.databasePath}) {
_initializeDatabaseFactory();
}
void _initializeDatabaseFactory() {
if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
// 初始化数据库连接
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
}
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
// `getDatabasePath` 能够初始化数据库路径, 在 mac window linux 都有其默认数据库存储路径
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'domitory.db');
// 查看数据库是否存在
final exists = await File(path).exists();
// 在本项目中数据库放置于 `assets` 目录下,所以默认路径下没有
if (!exists) {
// 当默认路径下没有数据库文件的时候,就将 assets 下的数据库拷贝到指定路径中 `await File(path).writeAsBytes(bytes)`
final data = await rootBundle.load(databasePath);
final bytes = data.buffer.asUint8List();
await File(path).writeAsBytes(bytes);
} else {
print('Database already exists at $path');
}
// 开启数据库,如果数据库里是空的,就执行下文的所有 `create` 表操作
return openDatabase(
path,
version: 1,
onCreate: _onCreate,
onOpen: (db) async {
print("Database opened at $path");
await db.execute('PRAGMA foreign_keys = ON;');
print("Foreign key constraints enabled");
await _ensureTablesExist(db);
},
);
}
Future<void> _onCreate(Database db, int version) async {
await _createTables(db);
}
Future<void> _createTables(Database db) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS ROOM (
room_number INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE,
status TEXT CHECK(status IN ('live', 'vacate', 'off_live')) NOT NULL DEFAULT 'off_live',
since DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES USER(id)
)
''');
await db.execute('''
CREATE TABLE IF NOT EXISTS UNIT (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)
''');
await db.execute('''
CREATE TABLE IF NOT EXISTS USER (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
unit_id INTEGER NOT NULL,
treatment INTEGER NOT NULL,
FOREIGN KEY (unit_id) REFERENCES UNIT(id)
)
''');
print("Database tables created");
}
Future<void> _ensureTablesExist(Database db) async {
await _createTables(db);
final tables = ['ROOM', 'UNIT', 'USER'];
for (final table in tables) {
final count = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM $table'),
);
print('Table $table has $count rows');
}
}
Future<void> close() async {
if (_database != null) {
await _database!.close();
_database = null;
}
}
}
在 DatabaseInitializer 类中,定义了数据库的初始化方法
在别的 daos 类中,都需要调用上述类的方法进行数据库连接操作
# unit_dao.dart
final DatabaseInitializer _databaseInitializer; // 定义一个 `DatabaseInitializer` 类
// 使用该类初始化 `UnitDao` 类
UnitDao(this._databaseInitializer);
Future<List<Unit>> getUnits() async {
// 调用 `DatabaseInitializer` 的 `database` 方法以初始化数据库
final Database db = await _databaseInitializer.database;
// 读取 sqlite3 数据库里 unit 表的所有数据
final List<Map<String, dynamic>> maps = await db.query('unit');
return List.generate(maps.length, (i) => Unit.fromMap(maps[i]));
}
...
在 DatabaseInitializer 类的编写中有几个概念需要我们搞清楚
get作为关键字,可以直接定义类的 getter 方法,通过这个方法可以直接访问类的属性
// 直接定义 DatabaseInitializer 类的 databse 属性
// 所以别的类可以直接通过 _databaseInitializer.database 进行数据库初始化
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
- Future await async 之间的关系
首先贴上 参考资料
这里用到了 dart 的异步编程的概念,当我们进行 IO 操作的时候(不管是网络IO,数据库IO或者说是读写磁盘IO)
我们需要等待程序完成操作后才执行下一步语句
在上文的代码中,就是要连接上数据库才能够进行数据库读写操作,在这种情况下我们就需要使用异步编程
async 关键字与 await 关键字通常在一块出现
async 意味着该函数需要进行异步操作,而 await 关键字则告诉 dart 需要等待该语句执行完再进行下一步操作
在上文的代码中, _database = await _initDatabase(); 意思即为
等待 _initDatabase() 函数执行完后,再将 return 出的值赋给 _database 变量
Future 关键字则是,当使用异步编程并且需要返回值的时候,就需要使用 Future 关键词来命名函数
Future<Database> 意思为,该函数的返回类型定义为 Database 类型,这种写法应该可以称为泛型的写法
综上所述,我们已经构建好了必要的层级代码
构建测试代码
在 flutter 工程的默认路径下,我们可以看到有如下文件夹
❯ tree -L1
.
├── analysis_options.yaml
├── android
├── assets
├── build
├── devtools_options.yaml
├── dist
├── distribute_options.yaml
├── domitory_manager.iml
├── ios
├── lib
├── linux
├── macos
├── pubspec.lock
├── pubspec.yaml
├── README.md
├── test #测试代码文件夹
├── web
└── windows
12 directories, 7 files
构建测试代码的目的是为了检查上文的代码我们有没有正确编写
在本文中,我们使用了如下测试代码
❯ tree -L1 ./test
./test
├── dao_test #测试 dao 是否编写正确
├── database_initializer_test.dart #为测试进行 database 初始化操作
├── mock_repository_test #测试 repoeisory 是否编写正确
└── real_repository_test
4 directories, 1 file
首先我们看一看如何在测试中进行 database 初始化
# database_initializer_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:domitory_manager/daos/database_initializer.dart';
import 'package:sqflite/sqflite.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late DatabaseInitializer dbInitializer;
// 在测试开始的时候都会进入 setup 入口,所以在 setup 入口中定义数据库初始化方法并指定数据库进行测试
setUp(() async {
//调用生产数据库进行测试
dbInitializer = DatabaseInitializer(databasePath: 'assets/domitory.db');
await dbInitializer.database;
});
// 在测试结束的时候关闭数据库连接
tearDown(() async {
await dbInitializer.close();
});
test('Database can get all data', () async {
final db = await dbInitializer.database;
// 如果数据库为空则初始化数据库
final tables = ['ROOM', 'UNIT', 'USER'];
for (final table in tables) {
final result = await db.query(table);
print("$table Data: $result");
// Note: We're not expecting data to be non-empty, as it depends on your actual database content
}
});
test('Insert and retrieve data from USER table', () async {
final db = await dbInitializer.database;
// Insert a user
await db.insert('USER', {
'name': 'Test User',
'unit_id': 1,
'treatment': 1,
});
// Retrieve the user
final result = await db.query(
'USER',
where: 'name = ?',
whereArgs: ['Test User'],
);
expect(result.isNotEmpty, true);
expect(result.first['name'], 'Test User');
});
test('Ensure tables exist', () async {
final db = await dbInitializer.database;
final tables = ['ROOM', 'UNIT', 'USER'];
for (final table in tables) {
final count = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM $table'),
);
print('Table $table has $count rows');
expect(count, isNotNull);
}
});
}
我们再贴 repository 以及 dao 的测试示例代码
#./test/dao_test/user_dao_test.dart
import 'package:domitory_manager/daos/database_initializer.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:domitory_manager/daos/user_dao.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:domitory_manager/models/user.dart';
import 'package:domitory_manager/daos/unit_dao.dart';
import 'package:domitory_manager/models/unit.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late DatabaseInitializer dbInitializer;
late UserDao userDao;
late UnitDao unitDao;
late Database db;
// 构建示例数据
final List<User> users = [
User(name: 'User 1', unitId: 1, treatment: 1),
User(name: 'User 2', unitId: 1, treatment: 1),
User(name: 'User 3', unitId: 2, treatment: 2),
];
final List<Unit> units = [
Unit(id: 1, name: 'Unit A'),
Unit(id: 2, name: 'Unit B'),
];
// setupAll 为测试的入口函数,在进行测试之前先进行数据库初始化
setUpAll(() {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
});
// 在 dart 中测试是分组的,在这里,我们做一个内存数据库测试
group('in-memory database UserDao\'s test', () {
setUp(() async {
dbInitializer = DatabaseInitializer(databasePath: ':memory:');
db = await dbInitializer.database;
unitDao = UnitDao(dbInitializer);
userDao = UserDao(dbInitializer);
});
// 定义该组测试的时候我们该做什么操作,通常需要关闭数据库连接以及删除表
tearDown(() async {
await db.execute('DELETE FROM USER');
await db.execute('DELETE FROM UNIT');
});
// 定义具体的测试内容
test('insert and get user', () async {
final user = users[0];
final id = await userDao.insertUser(user);
final retrievedUser = await userDao.getUserById(id);
expect(retrievedUser, isNotNull);
expect(retrievedUser!.id, equals(id));
expect(retrievedUser.name, equals(user.name));
expect(retrievedUser.unitId, equals(user.unitId));
expect(retrievedUser.treatment, equals(user.treatment));
});
});
// 对实际数据库进行测试
group('on-disk database UserDao\'s test', () {
setUp(() async {
dbInitializer = DatabaseInitializer(databasePath: 'assets/domitory.db');
db = await dbInitializer.database;
userDao = UserDao(dbInitializer);
unitDao = UnitDao(dbInitializer);
final result = await userDao.getUsers();
for (var tmp in result) {
print(tmp.toMap());
}
print("====================");
// insert an user
userDao.insertUser(users[0]);
final result1 = await userDao.getUsers();
for (var tmp in result1) {
print(tmp.toMap());
}
await db.execute('DELETE FROM USER');
await db.execute('DELETE FROM UNIT');
for (var unit in units) {
await unitDao.insertUnit(unit);
}
for (var user in users) {
await userDao.insertUser(user);
}
});
tearDown(() async {
await db.execute('DELETE FROM USER');
await db.execute('DELETE FROM UNIT');
});
// 下文共定义了 get users 与 update user 的方法
test('get users', () async {
final retrievedUsers = await userDao.getUsers();
expect(retrievedUsers.length, 3);
// Sort both lists by name to ensure consistent ordering
final sortedRetrievedUsers =
retrievedUsers..sort((a, b) => a.name.compareTo(b.name));
final sortedOriginalUsers =
users..sort((a, b) => a.name.compareTo(b.name));
for (int i = 0; i < sortedRetrievedUsers.length; i++) {
expect(
sortedRetrievedUsers[i].name,
equals(sortedOriginalUsers[i].name),
);
expect(
sortedRetrievedUsers[i].unitId,
equals(sortedOriginalUsers[i].unitId),
);
expect(
sortedRetrievedUsers[i].treatment,
equals(sortedOriginalUsers[i].treatment),
);
expect(sortedRetrievedUsers[i].id, isNotNull);
}
});
test('update user', () async {
final users = await userDao.getUsers();
final updateUser = users[0].copyWith(name: 'Updated User 1');
final id = await userDao.updateUser(updateUser);
final result = await userDao.getUserById(updateUser.id!);
expect(result, isNotNull);
expect(result, updateUser);
});
});
// 定义所有测试结束后该做什么
tearDownAll(() async {
final db = await dbInitializer.database;
await db.close();
});
}
# mock_room_dao.dart
// 首先定义一个供测试用的 dao 类
import 'package:mockito/mockito.dart';
import 'package:domitory_manager/daos/room_dao.dart';
import 'package:domitory_manager/models/room.dart';
class MockRoomDao extends Mock implements RoomDao {
final List<Room> _rooms = [];
@override
Future<List<Room>> getRooms() async {
return _rooms;
}
}
# user_repository_test.dart
// 再定义 repository 测试方法,测试 repository 有无编写正确
import 'package:flutter_test/flutter_test.dart';
import 'package:domitory_manager/repositories/room_repository.dart';
import 'package:domitory_manager/models/room.dart';
import 'mock_room_dao.dart';
void main() {
late RoomRepository roomRepository;
late MockRoomDao mockRoomDao;
final rooms = [
Room(
roomNumber: 101,
status: RoomStatus.off_live,
since: DateTime.tryParse('2023-12-12'),
),
Room(
roomNumber: 102,
userId: 1,
status: RoomStatus.live,
since: DateTime.tryParse('2024-12-12'),
),
Room(
roomNumber: 103,
userId: 2,
status: RoomStatus.vacate,
since: DateTime.tryParse('2021-02-22'),
),
];
setUp(() {
mockRoomDao = MockRoomDao();
roomRepository = RoomRepository(mockRoomDao);
});
group('RoomRepository', () {
test('getAllRooms return all rooms', () async {
await mockRoomDao.insertRoom(rooms[0]);
await mockRoomDao.insertRoom(rooms[1]);
await mockRoomDao.insertRoom(rooms[2]);
final result = await roomRepository.getRooms();
expect(result, rooms);
});
});
}
敲入 flutter test 查看测试是否成功
至此为止,针对编写好的后端内容,我们已经进行了成功的测试
总结
通过上述的编写,我们已经成功构建了 dart 应用的后端,虽然目前为止都和 flutter 没什么关系,但是在构建 ui 的时候就需要使用到 flutter 框架了\
这个我们放在下一篇文章里讲

浙公网安备 33010602011771号