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,    // 汽水
}

页面架构

应用采用底部导航栏设计,包含四个主要页面:

  1. 水质记录页面:展示所有水质测试记录,支持搜索和筛选
  2. 鱼缸管理页面:管理多个鱼缸的基本信息和状态
  3. 维护记录页面:记录日常维护操作
  4. 统计分析页面:展示水质趋势和维护统计

详细实现步骤

第一步:项目初始化

创建新的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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐