Flutter车辆油耗记录器应用开发教程
项目简介
车辆油耗记录器是一款帮助车主记录和分析车辆加油信息的Flutter应用。通过简洁实用的界面,用户可以轻松记录每次加油的详细信息,并通过统计分析功能了解车辆的油耗表现和费用支出。
运行效果图




核心功能
- 加油记录管理:记录里程、油量、油价、费用等信息
- 油耗计算:自动计算百公里油耗
- 统计分析:总体统计、月度统计、油价分析
- 数据管理:支持编辑、删除、备份、导出功能
- 直观展示:卡片式布局,信息一目了然
技术特点
- 单文件架构,代码简洁易懂
- Material Design 3设计风格
- 响应式布局设计
- 完整的CRUD操作
- 实时计算和统计功能
架构设计
整体架构
核心功能
记录管理
统计分析
油耗计算
数据模型
FuelRecord
FuelTrackerApp
FuelTrackerHomePage
记录页面
统计页面
设置页面
添加记录对话框
记录详情对话框
编辑记录对话框
页面结构
应用采用底部导航栏设计,包含三个主要页面:
- 记录页面:加油记录的查看和管理
- 统计页面:油耗和费用的统计分析
- 设置页面:应用配置和数据管理
数据模型设计
FuelRecord(加油记录)
dart
class FuelRecord {
final String id; // 唯一标识
final DateTime date; // 加油日期
final double odometer; // 里程表读数(km)
final double fuelAmount; // 加油量(升)
final double fuelPrice; // 油价(元/升)
final double totalCost; // 总费用(元)
final String gasStation; // 加油站名称
final String notes; // 备注信息
}
数据模型特点
- 完整性:包含加油记录的所有关键信息
- 可扩展性:支持添加更多字段如燃油类型等
- 计算能力:支持油耗和费用的自动计算
- 易用性:提供便捷的数据访问方法
核心功能实现
1. 记录页面实现
页面头部设计
dart
Widget _buildRecordsHeader() {
final totalRecords = _fuelRecords.length;
final totalCost = _fuelRecords.fold(0.0, (sum, record) => sum + record.totalCost);
final totalFuel = _fuelRecords.fold(0.0, (sum, record) => sum + record.fuelAmount);
return Container(
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade600, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
children: [
// 应用标题
Row(
children: [
const Icon(Icons.local_gas_station, color: Colors.white, size: 32),
const SizedBox(width: 12),
const Expanded(
child: Text(
'车辆油耗记录器',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white),
),
),
],
),
const SizedBox(height: 20),
// 统计卡片
Row(
children: [
Expanded(child: _buildSummaryCard('总记录', '$totalRecords', '次', Icons.receipt)),
const SizedBox(width: 12),
Expanded(child: _buildSummaryCard('总费用', '${totalCost.toStringAsFixed(0)}', '元', Icons.attach_money)),
const SizedBox(width: 12),
Expanded(child: _buildSummaryCard('总油量', '${totalFuel.toStringAsFixed(0)}', '升', Icons.local_gas_station)),
],
),
],
),
);
}
记录卡片设计
dart
Widget _buildRecordCard(FuelRecord record, int index) {
// 计算油耗(如果不是第一条记录)
double? fuelConsumption;
if (index < _fuelRecords.length - 1) {
final sortedRecords = List<FuelRecord>.from(_fuelRecords)
..sort((a, b) => b.date.compareTo(a.date));
final nextRecord = sortedRecords[index + 1];
final distance = record.odometer - nextRecord.odometer;
if (distance > 0) {
fuelConsumption = (record.fuelAmount / distance) * 100;
}
}
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () => _showRecordDetails(record),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头部信息
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(25),
),
child: const Icon(Icons.local_gas_station, color: Colors.blue, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(record.dateString, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text('里程:${record.odometer.toStringAsFixed(0)}km'),
if (record.gasStation.isNotEmpty) Text(record.gasStation),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('¥${record.totalCost.toStringAsFixed(1)}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.red)),
Text('${record.fuelAmount.toStringAsFixed(1)}L'),
],
),
],
),
const SizedBox(height: 12),
// 详细信息标签
Row(
children: [
_buildInfoChip('油价', '¥${record.fuelPrice.toStringAsFixed(1)}/L', Colors.orange),
const SizedBox(width: 8),
if (fuelConsumption != null)
_buildInfoChip('油耗', '${fuelConsumption.toStringAsFixed(1)}L/100km', Colors.green),
],
),
if (record.notes.isNotEmpty) ...[
const SizedBox(height: 8),
Text(record.notes, style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
],
],
),
),
),
);
}
2. 添加记录对话框
表单设计
dart
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('添加加油记录'),
content: SizedBox(
width: 400,
height: 500,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: [
// 日期选择
ListTile(
leading: const Icon(Icons.calendar_today),
title: const Text('日期'),
subtitle: Text('${_selectedDate.year}年${_selectedDate.month}月${_selectedDate.day}日'),
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
);
if (date != null) {
setState(() => _selectedDate = date);
}
},
),
// 里程表读数
TextFormField(
controller: _odometerController,
decoration: const InputDecoration(
labelText: '里程表读数 (km)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.speed),
),
keyboardType: TextInputType.number,
validator: (value) => value?.isEmpty == true ? '请输入里程表读数' : null,
),
// 加油量
TextFormField(
controller: _fuelAmountController,
decoration: const InputDecoration(
labelText: '加油量 (升)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.local_gas_station),
),
keyboardType: TextInputType.number,
onChanged: (_) => _calculateTotalCost(),
validator: (value) => value?.isEmpty == true ? '请输入加油量' : null,
),
// 油价
TextFormField(
controller: _fuelPriceController,
decoration: const InputDecoration(
labelText: '油价 (元/升)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
),
keyboardType: TextInputType.number,
onChanged: (_) => _calculateTotalCost(),
validator: (value) => value?.isEmpty == true ? '请输入油价' : null,
),
// 总费用显示
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'总费用: ¥${_totalCost.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue),
textAlign: TextAlign.center,
),
),
],
),
),
),
),
);
}
实时计算功能
dart
void _calculateTotalCost() {
final fuelAmount = double.tryParse(_fuelAmountController.text) ?? 0;
final fuelPrice = double.tryParse(_fuelPriceController.text) ?? 0;
setState(() {
_totalCost = fuelAmount * fuelPrice;
});
}
3. 统计分析功能
总体统计
dart
Widget _buildOverallStats() {
if (_fuelRecords.isEmpty) return const Card(child: Padding(padding: EdgeInsets.all(16), child: Text('暂无数据')));
final totalCost = _fuelRecords.fold(0.0, (sum, record) => sum + record.totalCost);
final totalFuel = _fuelRecords.fold(0.0, (sum, record) => sum + record.fuelAmount);
final avgPrice = totalCost / totalFuel;
// 计算平均油耗
double avgConsumption = 0;
int consumptionCount = 0;
final sortedRecords = List<FuelRecord>.from(_fuelRecords)
..sort((a, b) => a.date.compareTo(b.date));
for (int i = 1; i < sortedRecords.length; i++) {
final distance = sortedRecords[i].odometer - sortedRecords[i-1].odometer;
if (distance > 0) {
avgConsumption += (sortedRecords[i].fuelAmount / distance) * 100;
consumptionCount++;
}
}
if (consumptionCount > 0) {
avgConsumption /= consumptionCount;
}
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('总体统计', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildStatItem('总费用', '¥${totalCost.toStringAsFixed(0)}', Colors.red)),
Expanded(child: _buildStatItem('总油量', '${totalFuel.toStringAsFixed(0)}L', Colors.blue)),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(child: _buildStatItem('平均油价', '¥${avgPrice.toStringAsFixed(1)}/L', Colors.orange)),
Expanded(child: _buildStatItem('平均油耗', '${avgConsumption.toStringAsFixed(1)}L/100km', Colors.green)),
],
),
],
),
),
);
}
月度统计
dart
Widget _buildMonthlyStats() {
final now = DateTime.now();
final thisMonth = _fuelRecords.where((record) =>
record.date.year == now.year && record.date.month == now.month).toList();
final monthCost = thisMonth.fold(0.0, (sum, record) => sum + record.totalCost);
final monthFuel = thisMonth.fold(0.0, (sum, record) => sum + record.fuelAmount);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('本月统计 (${now.month}月)', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildStatItem('本月费用', '¥${monthCost.toStringAsFixed(0)}', Colors.red)),
Expanded(child: _buildStatItem('本月油量', '${monthFuel.toStringAsFixed(0)}L', Colors.blue)),
Expanded(child: _buildStatItem('加油次数', '${thisMonth.length}次', Colors.purple)),
],
),
],
),
),
);
}
油价统计
dart
Widget _buildFuelPriceStats() {
if (_fuelRecords.isEmpty) return const SizedBox();
final prices = _fuelRecords.map((r) => r.fuelPrice).toList()..sort();
final minPrice = prices.first;
final maxPrice = prices.last;
final avgPrice = prices.fold(0.0, (sum, price) => sum + price) / prices.length;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('油价统计', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildStatItem('最低油价', '¥${minPrice.toStringAsFixed(1)}/L', Colors.green)),
Expanded(child: _buildStatItem('最高油价', '¥${maxPrice.toStringAsFixed(1)}/L', Colors.red)),
Expanded(child: _buildStatItem('平均油价', '¥${avgPrice.toStringAsFixed(1)}/L', Colors.orange)),
],
),
],
),
),
);
}
UI组件设计
1. 统计卡片组件
dart
Widget _buildSummaryCard(String title, String value, String unit, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(icon, color: Colors.white, size: 20),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.white70)),
const SizedBox(height: 2),
Text(title, style: const TextStyle(fontSize: 12, color: Colors.white70)),
],
),
);
}
2. 信息标签组件
dart
Widget _buildInfoChip(String label, String value, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'$label: $value',
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.bold,
),
),
);
}
3. 统计项组件
dart
Widget _buildStatItem(String title, String value, Color color) {
return Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color)),
const SizedBox(height: 4),
Text(title, style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
],
),
);
}
对话框组件实现
1. 记录详情对话框
dart
class _RecordDetailsDialog extends StatelessWidget {
final FuelRecord record;
final Function(FuelRecord) onEdit;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
const Icon(Icons.local_gas_station, color: Colors.blue),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('加油记录'),
Text(record.dateString, style: TextStyle(fontSize: 14, color: Colors.grey.shade600)),
],
),
),
],
),
content: SizedBox(
width: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildDetailRow('里程表读数', '${record.odometer.toStringAsFixed(0)} km'),
_buildDetailRow('加油量', '${record.fuelAmount.toStringAsFixed(1)} 升'),
_buildDetailRow('油价', '¥${record.fuelPrice.toStringAsFixed(2)}/升'),
_buildDetailRow('总费用', '¥${record.totalCost.toStringAsFixed(2)}', isHighlight: true),
if (record.gasStation.isNotEmpty) _buildDetailRow('加油站', record.gasStation),
if (record.notes.isNotEmpty) _buildDetailRow('备注', record.notes),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('关闭')),
TextButton(onPressed: () => _showEditDialog(context), child: const Text('编辑')),
TextButton(
onPressed: () => _showDeleteConfirmation(context),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('删除'),
),
],
);
}
Widget _buildDetailRow(String label, String value, {bool isHighlight = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(label, style: TextStyle(fontSize: 14, color: Colors.grey.shade600)),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: isHighlight ? FontWeight.bold : FontWeight.normal,
color: isHighlight ? Colors.red : Colors.black87,
),
),
),
],
),
);
}
}
2. 编辑记录对话框
编辑对话框复用添加对话框的UI结构,但需要预填充现有数据:
dart
class _EditRecordDialog extends StatefulWidget {
final FuelRecord record;
final Function(FuelRecord) onSave;
@override
State<_EditRecordDialog> createState() => _EditRecordDialogState();
}
class _EditRecordDialogState extends State<_EditRecordDialog> {
@override
void initState() {
super.initState();
// 预填充现有数据
_odometerController = TextEditingController(text: widget.record.odometer.toString());
_fuelAmountController = TextEditingController(text: widget.record.fuelAmount.toString());
_fuelPriceController = TextEditingController(text: widget.record.fuelPrice.toString());
_gasStationController = TextEditingController(text: widget.record.gasStation);
_notesController = TextEditingController(text: widget.record.notes);
_selectedDate = widget.record.date;
_totalCost = widget.record.totalCost;
}
void _saveRecord() {
if (_formKey.currentState!.validate()) {
final editedRecord = widget.record.copyWith(
date: _selectedDate,
odometer: double.parse(_odometerController.text),
fuelAmount: double.parse(_fuelAmountController.text),
fuelPrice: double.parse(_fuelPriceController.text),
totalCost: _totalCost,
gasStation: _gasStationController.text.trim(),
notes: _notesController.text.trim(),
);
widget.onSave(editedRecord);
Navigator.pop(context);
}
}
}
核心算法实现
1. 油耗计算算法
dart
double? calculateFuelConsumption(FuelRecord current, FuelRecord previous) {
final distance = current.odometer - previous.odometer;
if (distance > 0) {
// 百公里油耗 = (加油量 / 行驶距离) * 100
return (current.fuelAmount / distance) * 100;
}
return null;
}
2. 统计计算算法
dart
class FuelStatistics {
static Map<String, dynamic> calculateOverallStats(List<FuelRecord> records) {
if (records.isEmpty) return {};
final totalCost = records.fold(0.0, (sum, record) => sum + record.totalCost);
final totalFuel = records.fold(0.0, (sum, record) => sum + record.fuelAmount);
final avgPrice = totalCost / totalFuel;
// 计算平均油耗
double avgConsumption = 0;
int consumptionCount = 0;
final sortedRecords = List<FuelRecord>.from(records)
..sort((a, b) => a.date.compareTo(b.date));
for (int i = 1; i < sortedRecords.length; i++) {
final distance = sortedRecords[i].odometer - sortedRecords[i-1].odometer;
if (distance > 0) {
avgConsumption += (sortedRecords[i].fuelAmount / distance) * 100;
consumptionCount++;
}
}
if (consumptionCount > 0) {
avgConsumption /= consumptionCount;
}
return {
'totalCost': totalCost,
'totalFuel': totalFuel,
'avgPrice': avgPrice,
'avgConsumption': avgConsumption,
'recordCount': records.length,
};
}
static Map<String, dynamic> calculateMonthlyStats(List<FuelRecord> records, DateTime month) {
final monthRecords = records.where((record) =>
record.date.year == month.year && record.date.month == month.month).toList();
final monthCost = monthRecords.fold(0.0, (sum, record) => sum + record.totalCost);
final monthFuel = monthRecords.fold(0.0, (sum, record) => sum + record.fuelAmount);
return {
'monthCost': monthCost,
'monthFuel': monthFuel,
'recordCount': monthRecords.length,
};
}
}
3. 数据验证算法
dart
class DataValidator {
static String? validateOdometer(String? value, double? lastOdometer) {
if (value == null || value.isEmpty) {
return '请输入里程表读数';
}
final odometer = double.tryParse(value);
if (odometer == null) {
return '请输入有效的数字';
}
if (odometer < 0) {
return '里程表读数不能为负数';
}
if (lastOdometer != null && odometer < lastOdometer) {
return '里程表读数不能小于上次记录';
}
return null;
}
static String? validateFuelAmount(String? value) {
if (value == null || value.isEmpty) {
return '请输入加油量';
}
final amount = double.tryParse(value);
if (amount == null) {
return '请输入有效的数字';
}
if (amount <= 0) {
return '加油量必须大于0';
}
if (amount > 200) {
return '加油量不能超过200升';
}
return null;
}
static String? validateFuelPrice(String? value) {
if (value == null || value.isEmpty) {
return '请输入油价';
}
final price = double.tryParse(value);
if (price == null) {
return '请输入有效的数字';
}
if (price <= 0) {
return '油价必须大于0';
}
if (price > 20) {
return '油价不能超过20元/升';
}
return null;
}
}
功能扩展建议
1. 数据持久化
dart
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
class FuelDataManager {
static const String _recordsKey = 'fuel_records';
static Future<void> saveRecords(List<FuelRecord> records) async {
final prefs = await SharedPreferences.getInstance();
final jsonList = records.map((record) => record.toJson()).toList();
await prefs.setString(_recordsKey, jsonEncode(jsonList));
}
static Future<List<FuelRecord>> loadRecords() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_recordsKey);
if (jsonString != null) {
final jsonList = jsonDecode(jsonString) as List;
return jsonList.map((json) => FuelRecord.fromJson(json)).toList();
}
return [];
}
static Future<void> exportToCSV(List<FuelRecord> records) async {
final csv = const ListToCsvConverter().convert([
['日期', '里程(km)', '加油量(L)', '油价(元/L)', '总费用(元)', '加油站', '备注'],
...records.map((record) => [
record.dateString,
record.odometer,
record.fuelAmount,
record.fuelPrice,
record.totalCost,
record.gasStation,
record.notes,
]),
]);
// 保存到文件
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/fuel_records_export.csv');
await file.writeAsString(csv);
}
}
2. 图表可视化
dart
import 'package:fl_chart/fl_chart.dart';
class FuelCharts {
static Widget buildConsumptionChart(List<FuelRecord> records) {
final consumptionData = <FlSpot>[];
final sortedRecords = List<FuelRecord>.from(records)
..sort((a, b) => a.date.compareTo(b.date));
for (int i = 1; i < sortedRecords.length; i++) {
final distance = sortedRecords[i].odometer - sortedRecords[i-1].odometer;
if (distance > 0) {
final consumption = (sortedRecords[i].fuelAmount / distance) * 100;
consumptionData.add(FlSpot(i.toDouble(), consumption));
}
}
return LineChart(
LineChartData(
gridData: FlGridData(show: true),
titlesData: FlTitlesData(show: true),
borderData: FlBorderData(show: true),
lineBarsData: [
LineChartBarData(
spots: consumptionData,
isCurved: true,
color: Colors.blue,
barWidth: 3,
dotData: FlDotData(show: true),
),
],
),
);
}
static Widget buildCostChart(List<FuelRecord> records) {
final costData = records.asMap().entries.map((entry) {
return FlSpot(entry.key.toDouble(), entry.value.totalCost);
}).toList();
return LineChart(
LineChartData(
gridData: FlGridData(show: true),
titlesData: FlTitlesData(show: true),
borderData: FlBorderData(show: true),
lineBarsData: [
LineChartBarData(
spots: costData,
isCurved: true,
color: Colors.red,
barWidth: 3,
dotData: FlDotData(show: true),
),
],
),
);
}
}
3. 多车辆支持
dart
class Vehicle {
final String id;
final String name;
final String brand;
final String model;
final int year;
final String licensePlate;
final double engineSize;
final String fuelType;
const Vehicle({
required this.id,
required this.name,
required this.brand,
required this.model,
required this.year,
required this.licensePlate,
required this.engineSize,
required this.fuelType,
});
}
class MultiVehicleManager {
static List<Vehicle> _vehicles = [];
static String? _currentVehicleId;
static void addVehicle(Vehicle vehicle) {
_vehicles.add(vehicle);
if (_currentVehicleId == null) {
_currentVehicleId = vehicle.id;
}
}
static void switchVehicle(String vehicleId) {
_currentVehicleId = vehicleId;
}
static Vehicle? get currentVehicle {
return _vehicles.firstWhere((v) => v.id == _currentVehicleId);
}
static List<FuelRecord> getRecordsForVehicle(String vehicleId, List<FuelRecord> allRecords) {
return allRecords.where((record) => record.vehicleId == vehicleId).toList();
}
}
4. 提醒功能
dart
class FuelReminder {
static Future<void> scheduleMaintenanceReminder(double currentOdometer) async {
final nextMaintenance = ((currentOdometer / 5000).ceil() * 5000).toDouble();
final remainingKm = nextMaintenance - currentOdometer;
if (remainingKm <= 500) {
await _showNotification(
'保养提醒',
'您的车辆还有${remainingKm.toInt()}公里需要保养',
);
}
}
static Future<void> checkFuelEfficiency(List<FuelRecord> records) async {
if (records.length < 3) return;
final recentRecords = records.take(3).toList();
double totalConsumption = 0;
int count = 0;
for (int i = 1; i < recentRecords.length; i++) {
final distance = recentRecords[i-1].odometer - recentRecords[i].odometer;
if (distance > 0) {
totalConsumption += (recentRecords[i-1].fuelAmount / distance) * 100;
count++;
}
}
if (count > 0) {
final avgConsumption = totalConsumption / count;
if (avgConsumption > 12) { // 假设正常油耗为12L/100km以下
await _showNotification(
'油耗异常',
'最近油耗偏高(${avgConsumption.toStringAsFixed(1)}L/100km),建议检查车辆',
);
}
}
}
static Future<void> _showNotification(String title, String body) async {
// 使用flutter_local_notifications显示通知
}
}
性能优化策略
1. 列表优化
dart
// 使用ListView.builder进行懒加载
Widget _buildRecordsList() {
final sortedRecords = List<FuelRecord>.from(_fuelRecords)
..sort((a, b) => b.date.compareTo(a.date));
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: sortedRecords.length,
itemBuilder: (context, index) {
final record = sortedRecords[index];
return _buildRecordCard(record, index);
},
);
}
// 使用AutomaticKeepAliveClientMixin保持页面状态
class _FuelTrackerHomePageState extends State<FuelTrackerHomePage>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return Scaffold(/* ... */);
}
}
2. 计算优化
dart
class FuelCalculationCache {
static final Map<String, double> _consumptionCache = {};
static final Map<String, Map<String, dynamic>> _statsCache = {};
static double? getCachedConsumption(String recordId) {
return _consumptionCache[recordId];
}
static void setCachedConsumption(String recordId, double consumption) {
_consumptionCache[recordId] = consumption;
}
static void clearCache() {
_consumptionCache.clear();
_statsCache.clear();
}
static Map<String, dynamic>? getCachedStats(String key) {
return _statsCache[key];
}
static void setCachedStats(String key, Map<String, dynamic> stats) {
_statsCache[key] = stats;
}
}
3. 内存管理
dart
class MemoryManager {
static void optimizeImageMemory() {
// 清理图片缓存
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
}
static void limitImageCacheSize() {
// 限制图片缓存大小
PaintingBinding.instance.imageCache.maximumSize = 100;
PaintingBinding.instance.imageCache.maximumSizeBytes = 50 << 20; // 50MB
}
}
测试指南
1. 单元测试
dart
// test/fuel_record_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:fuel_tracker/models/fuel_record.dart';
void main() {
group('FuelRecord', () {
test('should create fuel record with required fields', () {
final record = FuelRecord(
id: '1',
date: DateTime.now(),
odometer: 10000,
fuelAmount: 50,
fuelPrice: 7.5,
totalCost: 375,
);
expect(record.id, '1');
expect(record.odometer, 10000);
expect(record.fuelAmount, 50);
expect(record.totalCost, 375);
});
test('should calculate fuel consumption correctly', () {
final record1 = FuelRecord(
id: '1',
date: DateTime.now().subtract(const Duration(days: 7)),
odometer: 10000,
fuelAmount: 50,
fuelPrice: 7.5,
totalCost: 375,
);
final record2 = FuelRecord(
id: '2',
date: DateTime.now(),
odometer: 10500,
fuelAmount: 45,
fuelPrice: 7.8,
totalCost: 351,
);
final distance = record2.odometer - record1.odometer;
final consumption = (record2.fuelAmount / distance) * 100;
expect(consumption, closeTo(9.0, 0.1));
});
});
}
2. Widget测试
dart
// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fuel_tracker/main.dart';
void main() {
group('FuelTrackerApp', () {
testWidgets('should display navigation bar with 3 tabs', (WidgetTester tester) async {
await tester.pumpWidget(const FuelTrackerApp());
expect(find.byType(NavigationBar), findsOneWidget);
expect(find.text('记录'), findsOneWidget);
expect(find.text('统计'), findsOneWidget);
expect(find.text('设置'), findsOneWidget);
});
testWidgets('should show add button on records page', (WidgetTester tester) async {
await tester.pumpWidget(const FuelTrackerApp());
expect(find.byType(FloatingActionButton), findsOneWidget);
});
testWidgets('should open add record dialog when FAB is tapped', (WidgetTester tester) async {
await tester.pumpWidget(const FuelTrackerApp());
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('添加加油记录'), findsOneWidget);
});
});
}
部署指南
1. Android部署
yaml
# android/app/build.gradle
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.fuel_tracker"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
2. iOS部署
xml
<!-- ios/Runner/Info.plist -->
<key>CFBundleDisplayName</key>
<string>车辆油耗记录器</string>
<key>CFBundleIdentifier</key>
<string>com.example.fuelTracker</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
3. 构建命令
bash
# 构建Android APK
flutter build apk --release
# 构建Android App Bundle
flutter build appbundle --release
# 构建iOS
flutter build ios --release
项目总结
车辆油耗记录器应用通过Flutter框架实现了一个功能实用、界面简洁的油耗管理工具。应用具有以下特点:
技术亮点
- 简洁架构:单文件实现,代码结构清晰易懂
- 实用功能:涵盖加油记录、油耗计算、统计分析等核心需求
- 用户友好:直观的界面设计和便捷的操作流程
- 数据准确:实时计算和验证,确保数据的准确性
- 扩展性强:支持多种功能扩展和定制
功能完整性
- ✅ 加油记录管理
- ✅ 油耗自动计算
- ✅ 统计分析功能
- ✅ 数据编辑删除
- ✅ 费用跟踪
- ✅ 直观数据展示
应用价值
- 实用性强:解决车主日常油耗记录和分析需求
- 操作简单:界面直观,操作便捷
- 数据准确:自动计算,减少人工错误
- 分析全面:多维度统计,帮助优化用车成本
通过本教程的学习,开发者可以快速掌握Flutter应用开发的实用技能,并创建出满足实际需求的移动应用。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net