flutter_for_openharmonyFillUp -油耗追踪器app实战+车辆详情实现
第 06、07 篇我们已经把“车辆列表 + 添加车辆”跑通。但一个真正可长期使用的工具,不能只有列表。这一篇我们只做一件事:把讲透。从路由入口、参数传递、页面布局,到复用组件_KVRow,再到两个关键的下钻按钮。说明:本文所有代码片段都来自项目文件。

第 06、07 篇我们已经把“车辆列表 + 添加车辆”跑通。
但一个真正可长期使用的工具,不能只有列表。
车辆详情页(Vehicle Detail)承担两个任务:
- 把“车辆”当作一个实体,给用户一个确认入口。
- 作为下钻中心,把围绕车辆的功能组织起来。
这一篇我们只做一件事:把 VehicleDetailPage 讲透。
从路由入口、参数传递、页面布局,到复用组件 _KVRow,再到两个关键的下钻按钮。
说明:本文所有代码片段都来自项目文件 lib/app/fillup_app.dart。
1. 车辆详情页的定位:不是“看一眼车牌”,而是功能枢纽
很多项目把车辆详情写成:
- 一张卡片
- 一个车牌
- 完事
这样做的问题是:
- 用户看完没事可做
- 功能入口散落在各处
在 FillUp 里,我把车辆详情页当作“以车为中心的功能枢纽”。
最直接的两个动作是:
- 查看该车加油记录
- 进入维护计划(Feature 占位)
2. 从哪里进入车辆详情:车辆列表项 onTap
车辆详情页的入口在 VehiclesPage 的列表项 InkWell 上。
真实代码摘录:
onTap: () => Get.toNamed('/vehicle/detail', arguments: v),
为什么这里传 arguments: v(而不是只传 id)?
- 详情页拿到的是完整对象
- 可以直接展示,不需要二次查库
这种方式对“详情页只展示基础字段”的场景非常划算。
3. 参数兜底:arguments 丢了也不能崩
只要你使用了路由参数传递,就必须考虑一种情况:
- 某个入口忘了带 arguments
- 或者你后期改了参数类型
所以 VehicleDetailPage.build 的开头做了兜底。
真实代码:
final vehicle = Get.arguments as Vehicle?;
if (vehicle == null) {
return const Scaffold(body: Center(child: Text('无车辆数据')));
}
这段代码有两个工程价值:
- 避免崩溃:宁可显示提示,也不要直接 throw。
- 便于定位入口问题:看到“无车辆数据”,你就知道是路由参数链路断了。
4. 页面骨架:Scaffold + AppBar + ListView
车辆详情页的 Scaffold 很朴素,但结构清晰。
真实代码摘录:
return Scaffold(
appBar: AppBar(title: const Text('车辆详情')),
body: ListView(
padding: EdgeInsets.all(16.w),
children: <Widget>[
Card(
child: Padding(
padding: EdgeInsets.all(12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(vehicle.name, style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 8.h),
_KVRow(k: '车牌/备注', v: vehicle.plateNo),
],
),
),
),
SizedBox(height: 12.h),
FilledButton.tonalIcon(
onPressed: () {
Get.toNamed('/fillups', arguments: vehicle);
},
icon: const Icon(Icons.local_gas_station),
label: const Text('查看该车加油记录'),
),
SizedBox(height: 12.h),
FilledButton.tonalIcon(
onPressed: () {
Get.toNamed('/feature/maintenance_plan', arguments: vehicle);
},
icon: const Icon(Icons.build),
label: const Text('维护计划'),
),
],
),
);
我在详情页继续使用 ListView,原因和“添加车辆页”一致:
- 详情页以后一定会加更多信息
- 用 ListView 省掉后期改布局的成本
5. 基础信息展示:Card + Padding + Column
车辆详情的第一块内容是一张卡片。
它的目标是:
- 让用户确认“我正在看哪辆车”
- 让信息密度足够高,但又不显得拥挤
真实代码摘录:
Card(
child: Padding(
padding: EdgeInsets.all(12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(vehicle.name, style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 8.h),
_KVRow(k: '车牌/备注', v: vehicle.plateNo),
],
),
),
),
这里我只放了两行信息:
- 第一行:
vehicle.name,用titleLarge强化主标题 - 第二行:
_KVRow展示“车牌/备注”
为什么不直接 Text(vehicle.plateNo)?
- 因为详情页字段会越来越多
- 用统一的 key-value 组件更可控
6. _KVRow:把字段展示做成可复用的小组件
_KVRow 不是为了炫技。
它解决的问题是:
- key 左对齐
- value 自适应
- 多行文本不乱
真实实现如下(完整摘录):
class _KVRow extends StatelessWidget {
final String k;
final String v;
const _KVRow({required this.k, required this.v});
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: EdgeInsets.only(bottom: 6.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(
width: 84.w,
child: Text(
k,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: cs.onSurfaceVariant),
),
),
Expanded(child: Text(v)),
],
),
);
}
}
我在这里最看重两点:
SizedBox(width: 84.w)固定 key 的宽度Expanded(child: Text(v))让 value 自动换行
当你后续在车辆详情里加更多字段时,只需要重复:
_KVRow(k: 'xxx', v: 'yyy')
而不需要手写对齐逻辑。
7. 下钻按钮 1:查看该车加油记录
车辆详情页的第一个下钻动作是“查看该车加油记录”。
它是用户最常用的路径之一。
真实代码摘录:
FilledButton.tonalIcon(
onPressed: () {
Get.toNamed('/fillups', arguments: vehicle);
},
icon: const Icon(Icons.local_gas_station),
label: const Text('查看该车加油记录'),
),
这里依然是 传对象 的策略:
arguments: vehicle
后续 FillUpsPage 里可以根据 vehicles.activeVehicle 或 arguments 决定展示逻辑。
在这个项目里,记录页更多依赖全局 controller 的 activeVehicle。
但把 vehicle 传过去能让未来扩展更灵活。
8. 下钻按钮 2:维护计划(Feature 占位)
第二个按钮是“维护计划”,它代表一种架构预留:
- 详情页不应该只负责展示
- 它也应该提供“下一步能做什么”的入口
真实代码摘录:
FilledButton.tonalIcon(
onPressed: () {
Get.toNamed('/feature/maintenance_plan', arguments: vehicle);
},
icon: const Icon(Icons.build),
label: const Text('维护计划'),
),
注意这个路由风格:
/feature/<id>
这和项目里的 Feature Registry 一致。
占位按钮的价值是:
- 先把信息架构确定
- 后续把占位页替换成真实实现即可
9. Spacing 是一种语言:为什么我用 12.h/16.w 这种“可复用节奏”
你会看到详情页里有几处固定的间距:
padding: EdgeInsets.all(16.w),
padding: EdgeInsets.all(12.w),
SizedBox(height: 12.h),
它们的价值不是“看起来整齐”,而是让整页形成稳定节奏:
- 页面级 padding:16
- 卡片内部 padding:12
- 区块间距:12
当你以后要加第三个入口按钮或加更多信息行,只要沿用这套节奏,页面不会突然变得拥挤。
10. 车辆详情为什么仍然要“少”:把复杂度留给下一层
车辆详情页里我只展示了:
- 车辆名称
- 车牌/备注
并提供两个强相关入口。
原因是:
- 车辆详情页的第一职责是“确认 + 下钻”
- 过度堆字段会让用户不知道下一步干嘛
更复杂的信息(例如:该车总花费、总油量、段数、平均油耗)
更适合放在统计页,或者在“查看该车加油记录”的列表页顶部以卡片形式呈现。
11. 未来扩展:车辆详情页怎么加“编辑车辆”
当前项目没有实现车辆编辑。
但如果你要加,我建议你沿用我们在加油编辑页里用过的思路:
- 通过
Get.arguments把 Vehicle 传进编辑页 - 预填 controller 文本
- 保存时保持 id 不变
- controller 继续用
addOrUpdate(DB 层 upsert)
这样你不需要在数据层额外加 update。
12. 车辆详情与记录页的联动:从详情跳转后,记录页如何处理“未选择车辆”
车辆详情里我们跳转到了 /fillups。
但在实际使用中,记录页可能遇到两种空态:
- 没有选择车辆
- 选择了车辆但没有记录
FillUpsPage 在 Obx 里做了显式处理(真实代码摘录):
final v = vehicles.activeVehicle;
final list = v == null ? <FillUpEntry>[] : fillups.displayEntries(v.id);
if (v == null) {
return const _EmptyState(
title: '未选择车辆',
subtitle: '先去“车辆管理”添加/选择车辆',
icon: Icons.directions_car,
);
}
if (list.isEmpty) {
return const _EmptyState(
title: '暂无加油记录',
subtitle: '点击右下角 + 新增第一条记录(或取消筛选)',
icon: Icons.local_gas_station,
);
}
这一段逻辑对“车辆详情 -> 记录页”的体验很关键:
- 如果用户还没设置 activeVehicle,记录页会引导回车辆管理
- 如果用户已有车辆但没数据,记录页会引导去新增第一条记录
换句话说:详情页负责把用户带到下一层能力;下一层页面负责把用户继续往前推。
小结
车辆详情页要做得“短而硬”。
它不是信息堆砌,而是把路径组织清楚:
- 从车辆列表进详情(带 Vehicle 参数)
- 兜底避免崩溃
- 卡片展示基础信息
_KVRow做字段对齐- 两个下钻按钮把用户带到下一层能力
下一篇我们会继续做第 09 篇:统计概览实现,把“围绕车辆的数字”真正展示出来。
文章底部添加社区引导:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)