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 生成的,主要的改动在 dependenciesassets 中, 进行一定的参考即可
在调整好 pubspec.yaml 的内容后,敲入 flutter pub get 即可下载相关的依赖,如果依赖下载有问题参考本章开头的参考资料配置

至此,我们已经引入包括 sqlite3 在内的大部分重要依赖

构建层级代码

解决了依赖问题后需要在理论准备的基础上编写相关代码

理论准备 的各层级关系如下

flowchart TD repositories --> daos daos --> models

从底往上,先定义 models 再定义 daos 最后定义 repositories

  1. 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});
}
  1. 定义 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 类的编写中有几个概念需要我们搞清楚

  1. get 作为关键字,可以直接定义类的 getter 方法,通过这个方法可以直接访问类的属性
// 直接定义 DatabaseInitializer 类的 databse 属性
// 所以别的类可以直接通过 _databaseInitializer.database 进行数据库初始化
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }
  1. 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 框架了\

这个我们放在下一篇文章里讲

posted @ 2025-06-12 15:36  五花肉炒河粉  阅读(16)  评论(0)    收藏  举报