1. 项目概述:构建车辆管理模块的必要性
在开发任何与车辆相关的工具类应用时,我们都会面临一个基础但关键的问题:如何有效管理车辆实体。就像我在开发油耗追踪器应用时发现的那样,如果没有完善的车辆管理系统,所有后续功能(如加油记录、维护计划)都将成为无本之木。
这个模块需要同时解决三个层面的问题:
- 数据持久化:车辆信息如何存储、更新和删除
- 状态管理:当前选中车辆的状态同步
- 用户交互:完整的CRUD操作流程
我选择使用Flutter框架结合GetX状态管理库来实现这个模块,主要基于以下考量:
- Flutter的跨平台特性可以同时覆盖iOS和Android用户
- GetX提供了简洁高效的状态管理和依赖注入方案
- SQLite作为本地数据库足够支撑中小型数据量需求
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体架构分层
我将车辆管理模块划分为四个明确层级:
code复制┌────────────────┐
│ UI层 │ <- 页面展示与用户交互
├────────────────┤
│ Controller层 │ <- 业务逻辑与状态管理
├────────────────┤
│ Service层 │ <- 数据持久化操作
├────────────────┤
│ Model层 │ <- 数据结构定义
└────────────────┘
这种分层带来的主要优势是:
- 各层职责单一,便于维护
- 修改UI不影响底层逻辑
- 可以独立测试每一层
2.2 关键组件选型
数据库方案选择:
经过对比Hive、SharedPreferences和SQLite后,我最终选择sqflite作为本地存储方案,主要因为:
- 支持完整的SQL语法
- 具备事务处理能力
- 适合关系型数据结构
状态管理方案:
GetX相比Provider和Bloc的优势在于:
- 更简洁的语法(无需BuildContext)
- 内置依赖注入系统
- 天然支持响应式编程
3. 核心实现细节
3.1 数据模型设计
车辆模型采用最小字段集原则,只包含必要属性:
dart复制class Vehicle {
final String id; // 唯一标识
final String name; // 车辆名称
final String plateNo; // 车牌号
final int createdAtMs; // 创建时间戳
// 标准构造函数、create工厂方法、序列化方法...
}
实际项目中,你可以根据需求扩展字段(如车型、排量等),但建议初期保持精简。
3.2 数据库表结构
对应的SQLite表结构设计如下:
sql复制CREATE TABLE vehicles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
plate_no TEXT,
created_at_ms INTEGER NOT NULL
)
几个设计要点:
- 使用snake_case命名列(SQL惯例)
- 为必填字段添加NOT NULL约束
- 创建时间戳用于默认排序
3.3 数据访问层实现
数据库操作封装在FillUpDb类中,关键方法包括:
dart复制class FillUpDb {
// 初始化数据库
Future<Database> _open() async {...}
// 查询所有车辆(按创建时间倒序)
Future<List<Vehicle>> listVehicles() async {
final db = await _open();
final rows = await db.query('vehicles',
orderBy: 'created_at_ms DESC');
return rows.map(Vehicle.fromMap).toList();
}
// 新增或更新车辆
Future<void> upsertVehicle(Vehicle vehicle) async {
final db = await _open();
await db.insert('vehicles', vehicle.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
}
// 删除车辆及关联数据
Future<void> deleteVehicle(String vehicleId) async {
final db = await _open();
await db.delete('vehicles',
where: 'id = ?', whereArgs: [vehicleId]);
await db.delete('fillups',
where: 'vehicle_id = ?', whereArgs: [vehicleId]);
}
}
4. 状态管理与业务逻辑
4.1 Controller设计
VehiclesController负责管理车辆相关状态:
dart复制class VehiclesController extends GetxController {
final vehicles = <Vehicle>[].obs; // 车辆列表
final activeVehicleId = Rxn<String>(); // 当前选中车辆ID
// 刷新数据
Future<void> refreshAll() async {
final list = await FillUpDb.instance.listVehicles();
vehicles.value = list;
if (activeVehicleId.value == null && list.isNotEmpty) {
activeVehicleId.value = list.first.id; // 默认选中第一辆
}
}
// 获取当前选中车辆
Vehicle? get activeVehicle {
final id = activeVehicleId.value;
return id == null ? null : vehicles.firstWhereOrNull((v) => v.id == id);
}
// 删除车辆
Future<void> remove(String vehicleId) async {
await FillUpDb.instance.deleteVehicle(vehicleId);
if (activeVehicleId.value == vehicleId) {
activeVehicleId.value = null; // 清除当前选中状态
}
await refreshAll(); // 刷新列表
}
}
4.2 依赖注入配置
在应用启动时通过Bindings初始化Controller:
dart复制class FillUpBindings extends Bindings {
@override
void dependencies() {
Get.put(VehiclesController()); // 全局单例
// 其他Controller...
}
}
这种设计确保整个应用共享同一个车辆状态实例。
5. 用户界面实现
5.1 路由配置
使用GetX的路由系统定义车辆相关页面:
dart复制getPages: [
GetPage(name: '/vehicles', page: () => VehiclesPage()),
GetPage(name: '/vehicle/add', page: () => VehicleEditorPage()),
GetPage(name: '/vehicle/detail', page: () => VehicleDetailPage()),
// 其他页面...
]
路由命名遵循以下规范:
- 列表页使用复数形式(/vehicles)
- 详情页使用单数形式(/vehicle/detail)
- 操作动词明确(/vehicle/add)
5.2 车辆列表页
核心交互逻辑:
dart复制Obx(() {
if (ctl.vehicles.isEmpty) {
return _EmptyState(...); // 空状态提示
}
return ListView.separated(
itemCount: ctl.vehicles.length,
itemBuilder: (context, index) {
final vehicle = ctl.vehicles[index];
return InkWell(
onTap: () => Get.toNamed('/vehicle/detail', arguments: vehicle),
child: ListTile(
title: Text(vehicle.name),
subtitle: Text(vehicle.plateNo),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _confirmDelete(vehicle.id),
),
),
);
},
);
})
5.3 车辆添加/编辑页
表单处理关键代码:
dart复制final nameController = TextEditingController();
final plateController = TextEditingController();
// 保存逻辑
void _saveVehicle() {
final name = nameController.text.trim();
if (name.isEmpty) {
Get.snackbar('错误', '请输入车辆名称');
return;
}
final vehicle = Vehicle.create(
name: name,
plateNo: plateController.text.trim(),
);
ctl.addOrUpdate(vehicle);
Get.back(); // 自动返回
}
@override
void dispose() {
nameController.dispose();
plateController.dispose();
super.dispose();
}
6. 关键问题与解决方案
6.1 数据一致性问题
问题现象:
删除车辆后,关联的加油记录可能成为孤儿数据
解决方案:
在数据库层实现级联删除:
dart复制Future<void> deleteVehicle(String vehicleId) async {
final db = await _open();
await db.delete('vehicles', where: 'id = ?', whereArgs: [vehicleId]);
await db.delete('fillups', where: 'vehicle_id = ?', whereArgs: [vehicleId]);
}
6.2 状态同步问题
问题现象:
在多页面场景下,车辆选择状态不同步
解决方案:
使用全局状态管理:
dart复制// 在任何页面获取当前车辆
final currentVehicle = Get.find<VehiclesController>().activeVehicle;
// 切换车辆时自动通知所有监听者
void selectVehicle(String id) {
activeVehicleId.value = id;
}
6.3 性能优化
列表渲染优化:
使用ListView.separated而非ListView.builder,避免不必要的重建
数据库操作优化:
- 使用事务批量操作
- 建立适当索引
- 限制查询结果数量
7. 扩展性与维护建议
7.1 功能扩展方向
- 车辆分组:添加分类/标签功能
- 数据导入导出:支持CSV/Excel格式
- 多设备同步:集成云端备份
- 维护提醒:基于里程/时间的保养提醒
7.2 代码维护建议
- 单元测试覆盖:特别是数据库操作和业务逻辑
- 文档注释:为公开API添加详细文档
- 错误监控:集成Sentry等错误追踪工具
- 国际化准备:使用arb文件管理多语言
8. 实际开发中的经验教训
- 表单验证要尽早:在用户输入时即进行验证,而非仅在提交时
- 空状态设计很重要:特别是首次使用时的引导
- 删除操作需谨慎:建议添加回收站机制而非直接删除
- 数据库迁移方案:提前规划好版本升级路径
我在实际开发中遇到的一个典型问题是车辆删除后的状态同步。最初的设计是在页面层处理状态更新,这导致某些边缘场景下状态不同步。后来调整为在Controller层统一管理,问题得到彻底解决。
另一个值得分享的经验是关于表单设计。最初版本要求填写过多字段(如车型、购买日期等),导致用户流失率增高。简化到只保留必要字段后,使用体验明显改善。
