flutter_for_openharmonyFillUp -油耗追踪器app实战+筛选排序实现
前面我们已经把加油记录的新增/编辑/删除链路打通了。所有代码均来自。每段代码后面紧跟解释。

前面我们已经把加油记录的新增/编辑/删除链路打通了。
但当记录一多,用户的需求就会非常现实:
- 我只想看“加满”的记录(因为它影响油耗统计)。
- 我想按“花费/油量/里程”把记录快速排个序。
这一篇我们把“筛选 + 排序”做成一个完整闭环:
- 记录页顶部有一个快捷筛选按钮
- 记录页列表使用
displayEntries()自动应用筛选与排序 - 还有一个独立的“筛选/排序设置页”,用于更细的设置
所有代码均来自 lib/app/fillup_app.dart。
每段代码后面紧跟解释。
1. 把筛选/排序状态放在 controller:onlyFullTank + sortMode
筛选/排序不是某个页面的临时状态。
它应该在“记录页/设置页”之间共享。
项目里把它们放在 FillUpsController:
class FillUpsController extends GetxController {
final RxList<FillUpEntry> fillups = <FillUpEntry>[].obs;
final RxBool onlyFullTank = false.obs;
final RxString sortMode = 'date_desc'.obs;
解释:
onlyFullTank:是否仅显示“加满”记录sortMode:排序策略字符串(默认date_desc)
这种设计的好处是:
- 你在设置页修改后,记录页能立刻生效
- 页面只关注展示,不在 UI 层散落一堆排序逻辑
2. setSortMode:把排序切换收敛成一个入口
排序策略是字符串,但不要在 UI 里直接写 sortMode.value = ...。
项目里提供了一个显式方法:
void setSortMode(String mode) {
sortMode.value = mode;
}
解释:
- 现在方法很简单,但它是“收敛点”。
- 以后如果你要加埋点/持久化/提示文案,只改这里。
3. displayEntries:筛选 + 排序的唯一入口
记录页真正使用的数据列表不是 fillups 原始 RxList。
而是通过 displayEntries(vehicleId) 派生出来。
真实代码如下(完整摘录):
List<FillUpEntry> displayEntries(String vehicleId) {
final list = fillups.where((e) => e.vehicleId == vehicleId).toList();
if (onlyFullTank.value) {
list.removeWhere((e) => !e.isFullTank);
}
int cmpNum(num a, num b) => a == b ? 0 : (a < b ? -1 : 1);
switch (sortMode.value) {
case 'date_asc':
list.sort((a, b) => a.dateMs.compareTo(b.dateMs));
break;
case 'cost_desc':
list.sort((a, b) => cmpNum(b.totalCost, a.totalCost));
break;
case 'cost_asc':
list.sort((a, b) => cmpNum(a.totalCost, b.totalCost));
break;
case 'liters_desc':
list.sort((a, b) => cmpNum(b.liters, a.liters));
break;
case 'liters_asc':
list.sort((a, b) => cmpNum(a.liters, b.liters));
break;
case 'odo_desc':
list.sort((a, b) => cmpNum(b.odometer, a.odometer));
break;
case 'odo_asc':
list.sort((a, b) => cmpNum(a.odometer, b.odometer));
break;
case 'date_desc':
default:
list.sort((a, b) => b.dateMs.compareTo(a.dateMs));
break;
}
return list;
}
这一段你可以拆成两个阶段:
3.1 先筛选
if (onlyFullTank.value) {
list.removeWhere((e) => !e.isFullTank);
}
解释:
- 先按车辆过滤,再根据
onlyFullTank做二次过滤。 - 使用
removeWhere能让筛选条件非常清晰。
这里也隐含了一个产品取舍:
- “仅看加满”是一个全局开关
- 它不会影响数据库,只影响显示
3.2 再排序
int cmpNum(num a, num b) => a == b ? 0 : (a < b ? -1 : 1);
解释:
cmpNum用来减少重复代码。- 对
num做比较时比直接写a.compareTo(b)更通用。
接下来是 switch(sortMode.value):
date_asc/date_desc用dateMscost_*用totalCostliters_*用litersodo_*用odometer
这样一来,你的 UI 只要改一个字符串,就能切换整个列表排序。
4. 记录页顶部:一个 IconButton 作为“仅看加满”的快捷入口
筛选功能不能藏太深。
项目在记录页 appBar 直接放了一个切换按钮。
真实代码如下:
actions: <Widget>[
Obx(() {
final onlyFull = fillups.onlyFullTank.value;
return IconButton(
tooltip: onlyFull ? '仅看加满' : '查看全部',
icon: Icon(onlyFull ? Icons.local_gas_station : Icons.filter_alt_off),
onPressed: () {
fillups.onlyFullTank.value = !onlyFull;
Get.snackbar(
'筛选已更新',
fillups.onlyFullTank.value ? '仅显示“加满”记录' : '显示全部记录',
snackPosition: SnackPosition.BOTTOM,
);
},
);
}),
解释:
Obx让按钮图标与 tooltip 随状态变化。- 这个按钮只做一件事:切换
onlyFullTank。 - 切换后用
Get.snackbar给即时反馈。
这里的文案还有一个小细节:
- 当列表为空时 subtitle 会提示“或取消筛选”。
- 这能减少用户困惑:
“我明明有记录,怎么啥都没了?”
5. 进入筛选/排序设置页:一个 tune 图标入口
当用户需要更复杂的排序策略时,
就进入专门的设置页。
真实代码如下:
IconButton(
icon: const Icon(Icons.tune),
onPressed: () => Get.toNamed('/feature/filter_sort'),
),
解释:
- 顶部按钮只负责“开关”。
- 复杂配置放在单独页面,避免 appBar 被塞爆。
6. 记录页列表:只消费 displayEntries 的结果
记录页 body 的关键点是:
- 永远不要直接展示 controller 的原始列表。
- 只展示
displayEntries()的结果。
真实代码如下:
body: 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,
);
}
return ListView.separated(
padding: EdgeInsets.all(16.w),
itemCount: list.length,
separatorBuilder: (_, __) => SizedBox(height: 10.h),
itemBuilder: (context, index) {
final e = list[index];
return _FillUpTile(
entry: e,
onTap: () => Get.toNamed('/fillup/detail', arguments: e),
);
},
);
}),
解释:
v == null时直接给空态,而不是让列表报错。list.isEmpty也给空态,并提醒“可能是筛选导致”。- 真正的列表展示只看
displayEntries(v.id)。
这就是“展示层不做业务”的落地:
- 业务策略在 controller
- 页面只负责响应和渲染
7. 筛选/排序设置页:Switch + Dropdown 组合
项目里把“筛选/排序”做成一个设置区块。
它的意义是:
- 用户不必记住图标含义
- 可以明确看到当前状态
真实代码如下(完整摘录):
List<Widget> _buildFilterSort(BuildContext context) {
final ctl = Get.find<FillUpsController>();
return <Widget>[
const _SectionHeader(title: '筛选/排序'),
SizedBox(height: 10.h),
Obx(() {
return SwitchListTile(
value: ctl.onlyFullTank.value,
onChanged: (v) => ctl.onlyFullTank.value = v,
title: const Text('仅显示加满记录'),
subtitle: const Text('影响“记录”页列表展示'),
);
}),
SizedBox(height: 12.h),
Obx(() {
return DropdownButtonFormField<String>(
value: ctl.sortMode.value,
decoration: const InputDecoration(labelText: '排序方式'),
items: const <DropdownMenuItem<String>>[
DropdownMenuItem(value: 'date_desc', child: Text('日期:新 → 旧')),
DropdownMenuItem(value: 'date_asc', child: Text('日期:旧 → 新')),
DropdownMenuItem(value: 'cost_desc', child: Text('花费:高 → 低')),
DropdownMenuItem(value: 'cost_asc', child: Text('花费:低 → 高')),
DropdownMenuItem(value: 'liters_desc', child: Text('油量:高 → 低')),
DropdownMenuItem(value: 'liters_asc', child: Text('油量:低 → 高')),
DropdownMenuItem(value: 'odo_desc', child: Text('里程:高 → 低')),
DropdownMenuItem(value: 'odo_asc', child: Text('里程:低 → 高')),
],
onChanged: (v) {
if (v != null) ctl.setSortMode(v);
},
);
}),
SizedBox(height: 12.h),
FilledButton.tonalIcon(
onPressed: () => Get.toNamed('/fillups'),
icon: const Icon(Icons.local_gas_station),
label: const Text('返回记录页查看效果'),
),
];
}
这段代码里有三个细节非常值得保留。
7.1 SwitchListTile 的语义非常直观
- 用户知道“开/关”意味着什么。
- subtitle 明确告诉用户会影响哪里。
7.2 Dropdown 的 items 与 displayEntries 的 switch 必须一致
你会看到 items 的 value:
date_desc/date_asccost_*用totalCostliters_*用litersodo_*用odometer
这些字符串必须跟 controller 的 switch(sortMode.value) 对齐。
否则你在 UI 选了一个模式,但列表不按预期排序。
7.3 onChanged 走 setSortMode
if (v != null) ctl.setSortMode(v);
解释:
- 用方法收敛,后续扩展更容易。
- 也避免在 UI 里到处写
sortMode.value = ...。
8. 体验闭环:为什么设置页要有“返回记录页”按钮
筛选/排序这种功能,用户往往要不断试。
如果你让用户手势返回,体验也还行。
但项目里多做了一步:
FilledButton.tonalIcon(
onPressed: () => Get.toNamed('/fillups'),
icon: const Icon(Icons.local_gas_station),
label: const Text('返回记录页查看效果'),
),
解释:
- 给“下一步”一个明确按钮,降低学习成本。
- 用户也不会忘记设置是为了影响记录页。
9. 小结
这一篇我们把“筛选/排序”做成了一个可维护的体系:
- 状态放在 controller(
onlyFullTank/sortMode) - 列表只消费
displayEntries()的结果 - 顶部提供快捷筛选按钮,设置页提供可视化配置
- 排序策略使用字符串 + switch,扩展成本低
下一篇我们会继续做一个“功能页”的完整实现:维护计划。
它会把“车辆、记录、提醒”三个概念串起来,形成更强的闭环。
文章底部添加社区引导:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)