Flutter 框架跨平台鸿蒙开发 - 鱼缸水质记录器应用开发教程
temperature, // 温度ph, // pH值ammonia, // 氨氮nitrite, // 亚硝酸盐nitrate, // 硝酸盐dissolvedOxygen, // 溶解氧hardness, // 硬度alkalinity, // 碱度本教程详细介绍了如何使用Flutter开发一个功能完整的鱼缸水质记录器应用。应用包含了水质监测、鱼缸管理、维护记录、数据分析等核心功能,采用了M
Flutter鱼缸水质记录器应用开发教程
项目概述
本教程将带你开发一个功能完整的Flutter鱼缸水质记录器应用。这款应用专为水族爱好者设计,提供水质监测记录、鱼缸管理、维护记录和数据分析等功能,帮助用户科学养鱼,确保鱼儿健康成长。
运行效果图



应用特色
- 水质参数监测:记录温度、pH值、氨氮、亚硝酸盐、硝酸盐、溶解氧等关键指标
- 智能异常检测:自动判断水质是否正常,及时发现问题
- 多鱼缸管理:支持管理多个鱼缸,记录不同鱼缸的信息和状态
- 维护记录追踪:记录换水、清洁、设备维护等日常维护操作
- 数据统计分析:提供水质趋势、维护频率等统计分析功能
- 鱼类健康档案:记录鱼类健康状况、疾病治疗等信息
- 照片记录功能:支持添加照片记录鱼缸状态
技术栈
- 框架:Flutter 3.x
- 语言:Dart
- UI组件:Material Design 3
- 状态管理:StatefulWidget
- 动画:AnimationController + FadeTransition
- 数据存储:内存存储(可扩展为本地数据库)
- 导航:NavigationBar
项目结构设计
核心数据模型
1. 水质记录模型(WaterQualityRecord)
class WaterQualityRecord {
final String id; // 唯一标识
final String tankId; // 关联的鱼缸ID
final DateTime testDate; // 测试日期
final double temperature; // 温度 (°C)
final double ph; // pH值
final double ammonia; // 氨氮 (ppm)
final double nitrite; // 亚硝酸盐 (ppm)
final double nitrate; // 硝酸盐 (ppm)
final double dissolvedOxygen; // 溶解氧 (mg/L)
final double hardness; // 硬度 (dGH)
final double alkalinity; // 碱度 (KH)
final String notes; // 备注
final List<String> photos; // 照片
bool isAbnormal; // 是否异常
}
2. 鱼缸信息模型(AquariumTank)
class AquariumTank {
final String id; // 唯一标识
final String name; // 鱼缸名称
final double volume; // 容量 (L)
final String type; // 类型:淡水、海水、草缸等
final List<String> fishSpecies; // 鱼类品种
final DateTime setupDate; // 建缸日期
final String filtrationSystem; // 过滤系统
final String lightingSystem; // 照明系统
final String heatingSystem; // 加热系统
final String notes; // 备注
final String imageUrl; // 鱼缸照片
bool isActive; // 是否活跃
}
3. 维护记录模型(MaintenanceRecord)
class MaintenanceRecord {
final String id; // 唯一标识
final String tankId; // 关联的鱼缸ID
final DateTime maintenanceDate; // 维护日期
final String type; // 维护类型:换水、清洁、设备维护等
final String description; // 描述
final double waterChangePercentage; // 换水百分比
final String equipmentMaintained; // 维护的设备
final String notes; // 备注
final List<String> photos; // 照片
}
4. 鱼类健康记录模型(FishHealthRecord)
class FishHealthRecord {
final String id; // 唯一标识
final String tankId; // 关联的鱼缸ID
final String fishSpecies; // 鱼类品种
final DateTime recordDate; // 记录日期
final String healthStatus; // 健康状态:健康、生病、死亡
final String symptoms; // 症状
final String treatment; // 治疗方法
final String medication; // 用药
final String notes; // 备注
final List<String> photos; // 照片
}
枚举定义
水质参数类型枚举
enum WaterParameter {
temperature, // 温度
ph, // pH值
ammonia, // 氨氮
nitrite, // 亚硝酸盐
nitrate, // 硝酸盐
dissolvedOxygen, // 溶解氧
hardness, // 硬度
alkalinity, // 碱度
}
鱼缸类型枚举
enum TankType {
freshwater, // 淡水
saltwater, // 海水
planted, // 草缸
reef, // 珊瑚缸
brackish, // 汽水
}
页面架构
应用采用底部导航栏设计,包含四个主要页面:
- 水质记录页面:展示所有水质测试记录,支持搜索和筛选
- 鱼缸管理页面:管理多个鱼缸的基本信息和状态
- 维护记录页面:记录日常维护操作
- 统计分析页面:展示水质趋势和维护统计
详细实现步骤
第一步:项目初始化
创建新的Flutter项目:
flutter create aquarium_monitor_app
cd aquarium_monitor_app
第二步:主应用结构
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '鱼缸水质记录器',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const AquariumHomePage(),
);
}
}
第三步:数据初始化
创建示例鱼缸和水质数据:
void _initializeData() {
_tanks = [
AquariumTank(
id: '1',
name: '客厅主缸',
volume: 200.0,
type: _getTankTypeName(TankType.freshwater),
fishSpecies: ['孔雀鱼', '红绿灯', '斑马鱼', '清道夫'],
setupDate: DateTime.now().subtract(const Duration(days: 180)),
filtrationSystem: '外置过滤器 + 生化棉',
lightingSystem: 'LED全光谱灯 12小时',
heatingSystem: '300W加热棒',
notes: '社区鱼混养缸,水草造景',
imageUrl: 'main_tank.jpg',
isActive: true,
),
// 更多鱼缸数据...
];
}
第四步:水质记录页面
水质记录卡片组件
Widget _buildWaterQualityCard(WaterQualityRecord record) {
final tank = _tanks.firstWhere((t) => t.id == record.tankId);
return Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () => _showRecordDetail(record),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tank.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
_formatDateTime(record.testDate),
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
),
// 水质状态标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: record.isAbnormal ? Colors.red : Colors.green,
borderRadius: BorderRadius.circular(12),
),
child: Text(
record.isAbnormal ? '异常' : '正常',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
// 关键参数显示
Row(
children: [
Expanded(
child: _buildParameterItem(
'温度',
'${record.temperature.toStringAsFixed(1)}°C',
_getParameterColor(record.temperature, 22, 28),
Icons.thermostat,
),
),
Expanded(
child: _buildParameterItem(
'pH值',
record.ph.toStringAsFixed(1),
_getParameterColor(record.ph, 6.5, 7.5),
Icons.science,
),
),
Expanded(
child: _buildParameterItem(
'氨氮',
'${record.ammonia.toStringAsFixed(2)}ppm',
record.ammonia <= 0.25 ? Colors.green : Colors.red,
Icons.warning,
),
),
],
),
],
),
),
),
);
}
参数显示组件
Widget _buildParameterItem(String label, String value, Color color, IconData icon) {
return Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
第五步:智能水质检测
水质异常检测算法
// 检查水质是否正常
bool checkWaterQuality() {
// 淡水鱼标准范围
if (temperature < 22 || temperature > 28) return false;
if (ph < 6.5 || ph > 7.5) return false;
if (ammonia > 0.25) return false;
if (nitrite > 0.25) return false;
if (nitrate > 40) return false;
if (dissolvedOxygen < 5) return false;
return true;
}
// 参数颜色判断
Color _getParameterColor(double value, double min, double max) {
if (value >= min && value <= max) {
return Colors.green;
} else if (value < min - (max - min) * 0.2 || value > max + (max - min) * 0.2) {
return Colors.red;
} else {
return Colors.orange;
}
}
水质标准参考
| 参数 | 理想范围 | 可接受范围 | 危险范围 |
|---|---|---|---|
| 温度 | 24-26°C | 22-28°C | <20°C或>30°C |
| pH值 | 6.8-7.2 | 6.5-7.5 | <6.0或>8.0 |
| 氨氮 | 0ppm | 0-0.25ppm | >0.5ppm |
| 亚硝酸盐 | 0ppm | 0-0.25ppm | >0.5ppm |
| 硝酸盐 | <20ppm | <40ppm | >80ppm |
| 溶解氧 | >6mg/L | >5mg/L | <3mg/L |
第六步:鱼缸管理功能
鱼缸卡片组件
Widget _buildTankCard(AquariumTank tank) {
final recentRecord = _waterQualityRecords
.where((r) => r.tankId == tank.id)
.fold<WaterQualityRecord?>(null, (prev, current) {
if (prev == null) return current;
return current.testDate.isAfter(prev.testDate) ? current : prev;
});
return Card(
elevation: 4,
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () => _showTankDetail(tank),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 鱼缸基本信息
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tank.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'${tank.volume.toStringAsFixed(0)}L • ${tank.type}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
],
),
),
// 水质状态
if (recentRecord != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: recentRecord.isAbnormal ? Colors.red : Colors.green,
borderRadius: BorderRadius.circular(12),
),
child: Text(
recentRecord.isAbnormal ? '水质异常' : '水质正常',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
// 鱼类品种标签
if (tank.fishSpecies.isNotEmpty)
Wrap(
spacing: 6,
runSpacing: 4,
children: tank.fishSpecies.take(4).map((species) => Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
species,
style: TextStyle(
color: Colors.blue.shade700,
fontSize: 10,
),
),
)).toList(),
),
// 最近水质数据
if (recentRecord != null) ...[
const Text('最近水质数据:'),
Row(
children: [
Expanded(
child: _buildParameterItem(
'温度',
'${recentRecord.temperature.toStringAsFixed(1)}°C',
_getParameterColor(recentRecord.temperature, 22, 28),
Icons.thermostat,
),
),
// 更多参数显示...
],
),
],
// 运行时间
Row(
children: [
Icon(Icons.calendar_today, size: 16),
const SizedBox(width: 4),
Text('建缸: ${_formatDate(tank.setupDate)}'),
const Spacer(),
Text('运行${DateTime.now().difference(tank.setupDate).inDays}天'),
],
),
],
),
),
),
);
}
第七步:维护记录功能
维护记录卡片
Widget _buildMaintenanceCard(MaintenanceRecord record) {
final tank = _tanks.firstWhere((t) => t.id == record.tankId);
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题行
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tank.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
_formatDateTime(record.maintenanceDate),
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getMaintenanceTypeColor(record.type),
borderRadius: BorderRadius.circular(8),
),
child: Text(
record.type,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
// 维护详情
Text(record.description),
// 换水信息
if (record.waterChangePercentage > 0)
Row(
children: [
Icon(Icons.water_drop, size: 16, color: Colors.blue.shade600),
const SizedBox(width: 4),
Text(
'换水: ${record.waterChangePercentage.toStringAsFixed(0)}%',
style: TextStyle(color: Colors.blue.shade600, fontSize: 12),
),
],
),
// 设备维护信息
if (record.equipmentMaintained.isNotEmpty && record.equipmentMaintained != '无')
Row(
children: [
Icon(Icons.build, size: 16, color: Colors.orange.shade600),
const SizedBox(width: 4),
Text(
'设备: ${record.equipmentMaintained}',
style: TextStyle(color: Colors.orange.shade600, fontSize: 12),
),
],
),
],
),
),
);
}
维护类型颜色映射
Color _getMaintenanceTypeColor(String type) {
switch (type) {
case '换水':
return Colors.blue;
case '清洁':
return Colors.green;
case '设备维护':
return Colors.orange;
case '添加药剂':
return Colors.purple;
default:
return Colors.grey;
}
}
第八步:统计分析功能
统计卡片组件
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
title,
style: TextStyle(
fontSize: 12,
color: color,
),
textAlign: TextAlign.center,
),
],
),
);
}
水质状态分布图表
// 水质状态分布
..._tanks.map((tank) {
final tankRecords = _waterQualityRecords
.where((r) => r.tankId == tank.id)
.toList();
final normalRecords = tankRecords.where((r) => !r.isAbnormal).length;
final abnormalRecords = tankRecords.where((r) => r.isAbnormal).length;
final totalRecords = tankRecords.length;
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(tank.name),
Text('$totalRecords条记录'),
],
),
// 进度条显示正常/异常比例
Row(
children: [
Expanded(
flex: normalRecords,
child: Container(
height: 8,
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
bottomLeft: Radius.circular(4),
),
),
),
),
if (abnormalRecords > 0)
Expanded(
flex: abnormalRecords,
child: Container(
height: 8,
decoration: const BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.only(
topRight: Radius.circular(4),
bottomRight: Radius.circular(4),
),
),
),
),
],
),
],
),
);
}).toList(),
第九步:搜索和筛选功能
多维度筛选
List<WaterQualityRecord> _getFilteredWaterQualityRecords() {
return _waterQualityRecords.where((record) {
// 搜索过滤
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
final tankName = _getTankName(record.tankId).toLowerCase();
if (!tankName.contains(query) &&
!record.notes.toLowerCase().contains(query)) {
return false;
}
}
// 鱼缸过滤
if (_selectedTankId != null && record.tankId != _selectedTankId) {
return false;
}
// 异常过滤
if (_showAbnormalOnly && !record.isAbnormal) {
return false;
}
return true;
}).toList()
..sort((a, b) => b.testDate.compareTo(a.testDate));
}
筛选对话框
void _showFilterDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('筛选记录'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('鱼缸:'),
Wrap(
spacing: 8,
children: [
FilterChip(
label: const Text('全部'),
selected: _selectedTankId == null,
onSelected: (selected) {
setState(() {
_selectedTankId = selected ? null : _selectedTankId;
});
},
),
..._tanks.map((tank) => FilterChip(
label: Text(tank.name),
selected: _selectedTankId == tank.id,
onSelected: (selected) {
setState(() {
_selectedTankId = selected ? tank.id : null;
});
},
)),
],
),
SwitchListTile(
title: const Text('仅显示异常记录'),
value: _showAbnormalOnly,
onChanged: (value) {
setState(() {
_showAbnormalOnly = value;
});
},
),
],
),
),
),
);
}
第十步:动画效果实现
淡入动画
void _setupAnimations() {
_fadeAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeAnimationController,
curve: Curves.easeInOut,
));
_fadeAnimationController.forward();
}
// 在build方法中使用
body: FadeTransition(
opacity: _fadeAnimation,
child: IndexedStack(
index: _selectedIndex,
children: [
_buildWaterQualityPage(),
_buildTanksPage(),
_buildMaintenancePage(),
_buildStatisticsPage(),
],
),
),
核心功能详解
1. 水质参数标准
应用内置了科学的水质参数标准:
// 淡水鱼水质标准
class WaterQualityStandards {
static const double minTemperature = 22.0;
static const double maxTemperature = 28.0;
static const double minPH = 6.5;
static const double maxPH = 7.5;
static const double maxAmmonia = 0.25;
static const double maxNitrite = 0.25;
static const double maxNitrate = 40.0;
static const double minDissolvedOxygen = 5.0;
// 海水鱼水质标准
static const double saltwaterMinPH = 8.0;
static const double saltwaterMaxPH = 8.4;
static const double saltwaterMinTemperature = 24.0;
static const double saltwaterMaxTemperature = 26.0;
}
2. 智能异常检测
// 综合水质评估
WaterQualityLevel assessWaterQuality(WaterQualityRecord record) {
int score = 0;
// 温度评分
if (record.temperature >= 22 && record.temperature <= 28) {
score += 20;
} else if (record.temperature >= 20 && record.temperature <= 30) {
score += 10;
}
// pH值评分
if (record.ph >= 6.5 && record.ph <= 7.5) {
score += 20;
} else if (record.ph >= 6.0 && record.ph <= 8.0) {
score += 10;
}
// 氨氮评分
if (record.ammonia <= 0.25) {
score += 20;
} else if (record.ammonia <= 0.5) {
score += 10;
}
// 亚硝酸盐评分
if (record.nitrite <= 0.25) {
score += 20;
} else if (record.nitrite <= 0.5) {
score += 10;
}
// 硝酸盐评分
if (record.nitrate <= 20) {
score += 20;
} else if (record.nitrate <= 40) {
score += 10;
}
// 根据总分判断水质等级
if (score >= 90) return WaterQualityLevel.excellent;
if (score >= 70) return WaterQualityLevel.good;
if (score >= 50) return WaterQualityLevel.fair;
return WaterQualityLevel.poor;
}
enum WaterQualityLevel {
excellent, // 优秀
good, // 良好
fair, // 一般
poor, // 差
}
3. 维护提醒算法
// 计算下次维护时间
DateTime calculateNextMaintenance(AquariumTank tank, List<MaintenanceRecord> records) {
final lastMaintenance = records
.where((r) => r.tankId == tank.id)
.fold<DateTime?>(null, (prev, current) {
if (prev == null) return current.maintenanceDate;
return current.maintenanceDate.isAfter(prev) ? current.maintenanceDate : prev;
});
if (lastMaintenance == null) {
return DateTime.now(); // 立即需要维护
}
// 根据鱼缸类型和大小计算维护间隔
int intervalDays;
if (tank.volume < 50) {
intervalDays = 7; // 小缸每周维护
} else if (tank.volume < 200) {
intervalDays = 14; // 中缸两周维护
} else {
intervalDays = 21; // 大缸三周维护
}
return lastMaintenance.add(Duration(days: intervalDays));
}
4. 数据统计算法
// 计算水质稳定性
double calculateWaterQualityStability(List<WaterQualityRecord> records) {
if (records.length < 2) return 1.0;
double totalVariation = 0.0;
int parameterCount = 0;
// 计算各参数的变异系数
final parameters = ['temperature', 'ph', 'ammonia', 'nitrite', 'nitrate'];
for (final parameter in parameters) {
final values = records.map((r) => _getParameterValue(r, parameter)).toList();
final mean = values.reduce((a, b) => a + b) / values.length;
final variance = values.map((v) => (v - mean) * (v - mean)).reduce((a, b) => a + b) / values.length;
final standardDeviation = math.sqrt(variance);
final coefficientOfVariation = standardDeviation / mean;
totalVariation += coefficientOfVariation;
parameterCount++;
}
final averageVariation = totalVariation / parameterCount;
return math.max(0.0, 1.0 - averageVariation);
}
// 获取参数值
double _getParameterValue(WaterQualityRecord record, String parameter) {
switch (parameter) {
case 'temperature': return record.temperature;
case 'ph': return record.ph;
case 'ammonia': return record.ammonia;
case 'nitrite': return record.nitrite;
case 'nitrate': return record.nitrate;
default: return 0.0;
}
}
性能优化
1. 列表优化
使用ListView.builder实现虚拟滚动:
ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: filteredRecords.length,
itemBuilder: (context, index) {
final record = filteredRecords[index];
return _buildWaterQualityCard(record);
},
)
2. 状态管理优化
合理使用setState,避免不必要的重建:
void _updateWaterQualityRecord(WaterQualityRecord record) {
setState(() {
final index = _waterQualityRecords.indexWhere((r) => r.id == record.id);
if (index != -1) {
_waterQualityRecords[index] = record;
}
});
}
3. 内存管理
及时释放动画控制器:
void dispose() {
_fadeAnimationController.dispose();
super.dispose();
}
4. 数据缓存策略
// 缓存筛选结果
List<WaterQualityRecord>? _cachedFilteredRecords;
String? _lastSearchQuery;
String? _lastSelectedTankId;
List<WaterQualityRecord> _getFilteredWaterQualityRecords() {
// 检查缓存是否有效
if (_cachedFilteredRecords != null &&
_lastSearchQuery == _searchQuery &&
_lastSelectedTankId == _selectedTankId) {
return _cachedFilteredRecords!;
}
// 重新计算筛选结果
_cachedFilteredRecords = _waterQualityRecords.where((record) {
// 筛选逻辑...
}).toList();
_lastSearchQuery = _searchQuery;
_lastSelectedTankId = _selectedTankId;
return _cachedFilteredRecords!;
}
扩展功能
1. 数据持久化
使用sqflite数据库保存数据:
dependencies:
sqflite: ^2.3.0
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), 'aquarium.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
);
}
Future<void> _onCreate(Database db, int version) async {
// 创建水质记录表
await db.execute('''
CREATE TABLE water_quality_records(
id TEXT PRIMARY KEY,
tank_id TEXT NOT NULL,
test_date TEXT NOT NULL,
temperature REAL NOT NULL,
ph REAL NOT NULL,
ammonia REAL NOT NULL,
nitrite REAL NOT NULL,
nitrate REAL NOT NULL,
dissolved_oxygen REAL NOT NULL,
hardness REAL NOT NULL,
alkalinity REAL NOT NULL,
notes TEXT,
is_abnormal INTEGER NOT NULL
)
''');
// 创建鱼缸表
await db.execute('''
CREATE TABLE tanks(
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
volume REAL NOT NULL,
type TEXT NOT NULL,
fish_species TEXT NOT NULL,
setup_date TEXT NOT NULL,
filtration_system TEXT,
lighting_system TEXT,
heating_system TEXT,
notes TEXT,
image_url TEXT,
is_active INTEGER NOT NULL
)
''');
// 创建维护记录表
await db.execute('''
CREATE TABLE maintenance_records(
id TEXT PRIMARY KEY,
tank_id TEXT NOT NULL,
maintenance_date TEXT NOT NULL,
type TEXT NOT NULL,
description TEXT NOT NULL,
water_change_percentage REAL NOT NULL,
equipment_maintained TEXT,
notes TEXT
)
''');
}
// 插入水质记录
Future<int> insertWaterQualityRecord(WaterQualityRecord record) async {
final db = await database;
return await db.insert('water_quality_records', record.toMap());
}
// 获取所有水质记录
Future<List<WaterQualityRecord>> getAllWaterQualityRecords() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query('water_quality_records');
return List.generate(maps.length, (i) => WaterQualityRecord.fromMap(maps[i]));
}
}
2. 图表可视化
集成fl_chart显示水质趋势:
dependencies:
fl_chart: ^0.64.0
Widget _buildWaterQualityChart(List<WaterQualityRecord> records) {
return Container(
height: 300,
padding: const EdgeInsets.all(16),
child: LineChart(
LineChartData(
gridData: FlGridData(show: true),
titlesData: FlTitlesData(show: true),
borderData: FlBorderData(show: true),
lineBarsData: [
// 温度曲线
LineChartBarData(
spots: records.asMap().entries.map((entry) {
return FlSpot(entry.key.toDouble(), entry.value.temperature);
}).toList(),
isCurved: true,
color: Colors.red,
barWidth: 2,
dotData: FlDotData(show: true),
),
// pH值曲线
LineChartBarData(
spots: records.asMap().entries.map((entry) {
return FlSpot(entry.key.toDouble(), entry.value.ph * 4); // 缩放显示
}).toList(),
isCurved: true,
color: Colors.blue,
barWidth: 2,
dotData: FlDotData(show: true),
),
],
),
),
);
}
3. 照片管理功能
集成image_picker进行照片管理:
dependencies:
image_picker: ^1.0.4
class PhotoManager {
final ImagePicker _picker = ImagePicker();
Future<String?> pickImage() async {
final XFile? image = await _picker.pickImage(source: ImageSource.camera);
if (image != null) {
// 保存图片到本地存储
final String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
final String localPath = await _saveImageToLocal(image.path, fileName);
return localPath;
}
return null;
}
Future<String> _saveImageToLocal(String imagePath, String fileName) async {
final Directory appDir = await getApplicationDocumentsDirectory();
final String localPath = '${appDir.path}/photos/$fileName';
// 创建目录
await Directory('${appDir.path}/photos').create(recursive: true);
// 复制文件
await File(imagePath).copy(localPath);
return localPath;
}
Widget buildPhotoGrid(List<String> photoPaths) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: photoPaths.length,
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(photoPaths[index]),
fit: BoxFit.cover,
),
);
},
);
}
}
4. 提醒通知功能
使用flutter_local_notifications设置提醒:
dependencies:
flutter_local_notifications: ^16.1.0
class NotificationService {
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings();
const settings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _notifications.initialize(settings);
}
Future<void> scheduleWaterTestReminder(AquariumTank tank) async {
await _notifications.zonedSchedule(
tank.id.hashCode,
'水质检测提醒',
'该检测${tank.name}的水质了',
_nextInstanceOfTime(20, 0), // 每天晚上8点
const NotificationDetails(
android: AndroidNotificationDetails(
'water_test_channel',
'水质检测提醒',
channelDescription: '提醒用户检测水质',
importance: Importance.high,
),
),
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
);
}
Future<void> scheduleMaintenanceReminder(AquariumTank tank, DateTime nextMaintenance) async {
await _notifications.zonedSchedule(
tank.id.hashCode + 1000,
'维护提醒',
'${tank.name}需要进行维护了',
tz.TZDateTime.from(nextMaintenance, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails(
'maintenance_channel',
'维护提醒',
channelDescription: '提醒用户进行鱼缸维护',
importance: Importance.high,
),
),
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
);
}
}
5. 数据导出功能
导出水质记录为CSV或PDF:
dependencies:
csv: ^5.0.2
pdf: ^3.10.4
class DataExporter {
Future<void> exportWaterQualityToCSV(List<WaterQualityRecord> records) async {
List<List<dynamic>> rows = [];
// 添加表头
rows.add([
'日期',
'鱼缸',
'温度(°C)',
'pH值',
'氨氮(ppm)',
'亚硝酸盐(ppm)',
'硝酸盐(ppm)',
'溶解氧(mg/L)',
'硬度(dGH)',
'碱度(KH)',
'状态',
'备注'
]);
// 添加数据行
for (final record in records) {
rows.add([
record.testDate.toString(),
_getTankName(record.tankId),
record.temperature,
record.ph,
record.ammonia,
record.nitrite,
record.nitrate,
record.dissolvedOxygen,
record.hardness,
record.alkalinity,
record.isAbnormal ? '异常' : '正常',
record.notes,
]);
}
String csv = const ListToCsvConverter().convert(rows);
// 保存文件
final Directory directory = await getApplicationDocumentsDirectory();
final String path = '${directory.path}/water_quality_export.csv';
final File file = File(path);
await file.writeAsString(csv);
// 分享文件
await Share.shareFiles([path], text: '水质记录导出');
}
Future<void> exportTankReportToPDF(AquariumTank tank, List<WaterQualityRecord> records) async {
final pdf = pw.Document();
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'${tank.name} 水质报告',
style: pw.TextStyle(
fontSize: 24,
fontWeight: pw.FontWeight.bold,
),
),
pw.SizedBox(height: 20),
pw.Text('鱼缸信息:'),
pw.Text('容量: ${tank.volume}L'),
pw.Text('类型: ${tank.type}'),
pw.Text('建缸日期: ${tank.setupDate}'),
pw.SizedBox(height: 20),
pw.Text('水质记录:'),
pw.Table.fromTextArray(
headers: ['日期', '温度', 'pH', '氨氮', '状态'],
data: records.map((r) => [
r.testDate.toString().substring(0, 10),
'${r.temperature}°C',
r.ph.toString(),
'${r.ammonia}ppm',
r.isAbnormal ? '异常' : '正常',
]).toList(),
),
],
);
},
),
);
final output = await getTemporaryDirectory();
final file = File('${output.path}/tank_report.pdf');
await file.writeAsBytes(await pdf.save());
await Share.shareFiles([file.path], text: '鱼缸水质报告');
}
}
6. 云同步功能
集成Firebase进行数据同步:
dependencies:
firebase_core: ^2.24.2
cloud_firestore: ^4.13.6
class FirebaseService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Future<void> syncWaterQualityRecords(List<WaterQualityRecord> records) async {
final batch = _firestore.batch();
for (final record in records) {
final docRef = _firestore
.collection('users')
.doc(_getCurrentUserId())
.collection('water_quality_records')
.doc(record.id);
batch.set(docRef, record.toMap());
}
await batch.commit();
}
Stream<List<WaterQualityRecord>> getWaterQualityRecordsStream() {
return _firestore
.collection('users')
.doc(_getCurrentUserId())
.collection('water_quality_records')
.orderBy('test_date', descending: true)
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => WaterQualityRecord.fromMap(doc.data()))
.toList());
}
Future<void> syncTanks(List<AquariumTank> tanks) async {
final batch = _firestore.batch();
for (final tank in tanks) {
final docRef = _firestore
.collection('users')
.doc(_getCurrentUserId())
.collection('tanks')
.doc(tank.id);
batch.set(docRef, tank.toMap());
}
await batch.commit();
}
String _getCurrentUserId() {
// 获取当前用户ID的逻辑
return 'user_id';
}
}
测试策略
1. 单元测试
测试核心业务逻辑:
// test/water_quality_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:aquarium_monitor_app/models/water_quality_record.dart';
void main() {
group('WaterQualityRecord Tests', () {
test('should detect normal water quality correctly', () {
final record = WaterQualityRecord(
id: '1',
tankId: '1',
testDate: DateTime.now(),
temperature: 25.0,
ph: 7.0,
ammonia: 0.0,
nitrite: 0.0,
nitrate: 20.0,
dissolvedOxygen: 6.0,
hardness: 8.0,
alkalinity: 4.0,
notes: '',
photos: [],
);
expect(record.checkWaterQuality(), isTrue);
expect(record.isAbnormal, isFalse);
});
test('should detect abnormal water quality correctly', () {
final record = WaterQualityRecord(
id: '1',
tankId: '1',
testDate: DateTime.now(),
temperature: 30.0, // 过高
ph: 8.5, // 过高
ammonia: 0.5, // 过高
nitrite: 0.0,
nitrate: 20.0,
dissolvedOxygen: 6.0,
hardness: 8.0,
alkalinity: 4.0,
notes: '',
photos: [],
);
expect(record.checkWaterQuality(), isFalse);
});
});
}
2. Widget测试
测试UI组件:
// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:aquarium_monitor_app/main.dart';
void main() {
testWidgets('App should display navigation bar', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
expect(find.byType(NavigationBar), findsOneWidget);
expect(find.text('水质记录'), findsOneWidget);
expect(find.text('鱼缸管理'), findsOneWidget);
expect(find.text('维护记录'), findsOneWidget);
expect(find.text('统计分析'), findsOneWidget);
});
testWidgets('Should show water quality records', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
// 等待数据加载
await tester.pumpAndSettle();
// 验证水质记录卡片存在
expect(find.byType(Card), findsWidgets);
expect(find.text('客厅主缸'), findsOneWidget);
});
}
3. 集成测试
测试完整用户流程:
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:aquarium_monitor_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('App Integration Tests', () {
testWidgets('Complete user flow test', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
// 测试搜索功能
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), '客厅');
await tester.tap(find.text('搜索'));
await tester.pumpAndSettle();
// 验证搜索结果
expect(find.text('客厅主缸'), findsOneWidget);
// 测试导航
await tester.tap(find.text('鱼缸管理'));
await tester.pumpAndSettle();
expect(find.text('鱼缸管理'), findsWidgets);
// 测试筛选功能
await tester.tap(find.text('水质记录'));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.filter_list));
await tester.pumpAndSettle();
await tester.tap(find.text('仅显示异常记录'));
await tester.tap(find.text('应用'));
await tester.pumpAndSettle();
// 验证筛选结果
expect(find.text('异常'), findsWidgets);
});
});
}
部署和发布
1. Android发布
# 生成签名密钥
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
# 配置android/app/build.gradle
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
# 构建APK
flutter build apk --release
2. iOS发布
# 构建iOS应用
flutter build ios --release
# 使用Xcode进行代码签名和发布
open ios/Runner.xcworkspace
3. Web发布
# 构建Web应用
flutter build web --release
# 部署到服务器
# 将build/web目录内容上传到Web服务器
总结
本教程详细介绍了如何使用Flutter开发一个功能完整的鱼缸水质记录器应用。应用包含了水质监测、鱼缸管理、维护记录、数据分析等核心功能,采用了Material Design 3设计规范,提供了良好的用户体验。
通过本项目的学习,你将掌握:
- Flutter应用架构设计
- 复杂数据模型的设计和管理
- 多页面导航和状态管理
- 搜索和筛选功能的实现
- 数据可视化和统计分析
- 智能检测算法的应用
- 动画效果的实现
- 性能优化技巧
- 测试策略和部署流程
这个应用不仅适合水族爱好者使用,也为其他监测类应用的开发提供了很好的参考模板。你可以根据自己的需求进行功能扩展和定制,比如添加IoT设备集成、AI水质分析、社区分享等高级功能。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)