Flutter实时紫外线强度查询:智能防护助手守护健康

项目概述

随着人们对健康生活的日益重视,紫外线防护已成为日常生活中不可忽视的重要环节。过度的紫外线暴露不仅会导致皮肤晒伤,还可能引发皮肤癌等严重疾病。本项目开发了一款基于Flutter的实时紫外线强度查询应用,旨在为用户提供准确、及时的紫外线指数信息,帮助用户科学防护,守护健康。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

核心功能特性

  • 实时紫外线监测:提供当前位置的实时紫外线指数和等级
  • 全国城市覆盖:支持全国主要城市的紫外线强度查询
  • 7天预报功能:未来一周的紫外线强度趋势预测
  • 智能防护建议:根据紫外线强度提供个性化防护指导
  • 预警提醒系统:紫外线强度异常时及时发出预警
  • 24小时趋势图:直观展示一天内紫外线强度变化
  • 环境信息集成:结合温度、湿度、空气质量等环境数据

应用价值

  1. 健康防护:科学指导用户进行紫外线防护,预防皮肤损伤
  2. 出行规划:帮助用户合理安排户外活动时间
  3. 个性化建议:根据不同紫外线强度提供针对性防护措施
  4. 预警保护:及时提醒用户紫外线强度异常情况

开发环境配置

系统要求

开发本应用需要满足以下环境要求:

  • 操作系统:Windows 10/11、macOS 10.14+、或 Ubuntu 18.04+
  • Flutter SDK:3.0.0 或更高版本
  • Dart SDK:2.17.0 或更高版本
  • 开发工具:Android Studio、VS Code 或 IntelliJ IDEA
  • 设备要求:Android 5.0+ 或 iOS 11.0+

Flutter环境搭建

1. 安装Flutter SDK
# Windows
# 下载flutter_windows_3.x.x-stable.zip并解压

# macOS
curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.x.x-stable.zip
unzip flutter_macos_3.x.x-stable.zip

# Linux
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.x.x-stable.tar.xz
tar xf flutter_linux_3.x.x-stable.tar.xz
2. 配置环境变量
# Windows (系统环境变量)
C:\flutter\bin

# macOS/Linux (添加到~/.bashrc或~/.zshrc)
export PATH="$PATH:/path/to/flutter/bin"
3. 验证安装
flutter doctor

确保所有检查项都通过。

项目初始化

1. 创建项目
flutter create uv_index_app
cd uv_index_app
2. 配置依赖

编辑pubspec.yaml文件:

name: uv_index_app
description: 实时紫外线强度查询应用

publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true
3. 安装依赖
flutter pub get

核心数据模型设计

UVData 紫外线数据模型

紫外线数据模型是应用的核心数据结构,包含了完整的紫外线和环境信息:

class UVData {
  final String cityId;              // 城市唯一标识
  final String cityName;            // 城市名称
  final double uvIndex;             // 紫外线指数
  final String uvLevel;             // 紫外线等级
  final DateTime updateTime;        // 更新时间
  final double latitude;            // 纬度
  final double longitude;           // 经度
  final String weather;             // 天气状况
  final int temperature;            // 温度
  final int humidity;               // 湿度
  final double cloudCover;          // 云量
  final int aqi;                   // 空气质量指数
  final String recommendation;      // 防护建议
  final List<String> protectionTips; // 防护措施

  UVData({
    required this.cityId,
    required this.cityName,
    required this.uvIndex,
    required this.uvLevel,
    required this.updateTime,
    required this.latitude,
    required this.longitude,
    required this.weather,
    required this.temperature,
    required this.humidity,
    required this.cloudCover,
    required this.aqi,
    required this.recommendation,
    required this.protectionTips,
  });

  // 计算属性:紫外线等级颜色
  Color get uvLevelColor {
    if (uvIndex <= 2) return Colors.green;      // 低
    if (uvIndex <= 5) return Colors.yellow;     // 中等
    if (uvIndex <= 7) return Colors.orange;     // 高
    if (uvIndex <= 10) return Colors.red;       // 很高
    return Colors.purple;                       // 极高
  }

  // 计算属性:紫外线等级图标
  IconData get uvLevelIcon {
    if (uvIndex <= 2) return Icons.wb_sunny_outlined;
    if (uvIndex <= 5) return Icons.wb_sunny;
    if (uvIndex <= 7) return Icons.warning_amber;
    if (uvIndex <= 10) return Icons.dangerous;
    return Icons.error;
  }

  // 计算属性:防护等级
  String get protectionLevel {
    if (uvIndex <= 2) return '无需防护';
    if (uvIndex <= 5) return '低度防护';
    if (uvIndex <= 7) return '中度防护';
    if (uvIndex <= 10) return '高度防护';
    return '极度防护';
  }
}

UVForecast 紫外线预报模型

用于存储未来几天的紫外线预报信息:

class UVForecast {
  final DateTime date;        // 日期
  final double maxUV;         // 最大紫外线指数
  final double minUV;         // 最小紫外线指数
  final String weather;       // 天气状况
  final int maxTemp;          // 最高温度
  final int minTemp;          // 最低温度

  UVForecast({
    required this.date,
    required this.maxUV,
    required this.minUV,
    required this.weather,
    required this.maxTemp,
    required this.minTemp,
  });

  // 计算属性:最大紫外线指数颜色
  Color get maxUVColor {
    if (maxUV <= 2) return Colors.green;
    if (maxUV <= 5) return Colors.yellow;
    if (maxUV <= 7) return Colors.orange;
    if (maxUV <= 10) return Colors.red;
    return Colors.purple;
  }
}

UVAlert 紫外线预警模型

用于存储紫外线预警信息:

class UVAlert {
  final String id;                    // 预警唯一标识
  final String title;                 // 预警标题
  final String message;               // 预警内容
  final String level;                 // 预警等级
  final DateTime issueTime;           // 发布时间
  final DateTime expireTime;          // 过期时间
  final List<String> affectedAreas;   // 影响区域

  UVAlert({
    required this.id,
    required this.title,
    required this.message,
    required this.level,
    required this.issueTime,
    required this.expireTime,
    required this.affectedAreas,
  });

  // 计算属性:预警颜色
  Color get alertColor {
    switch (level) {
      case '黄色预警': return Colors.yellow;
      case '橙色预警': return Colors.orange;
      case '红色预警': return Colors.red;
      default: return Colors.blue;
    }
  }

  // 计算属性:预警图标
  IconData get alertIcon {
    switch (level) {
      case '黄色预警': return Icons.warning_amber;
      case '橙色预警': return Icons.warning;
      case '红色预警': return Icons.dangerous;
      default: return Icons.info;
    }
  }
}

应用架构设计

整体架构

应用采用四标签页的架构设计,每个标签页专注于特定功能:

UVIndexHomePage 主页面

当前模块

城市模块

预报模块

预警模块

实时紫外线

环境信息

防护建议

24小时趋势

城市列表

紫外线对比

城市详情

7天预报

趋势分析

天气关联

预警信息

影响区域

防护指导

状态管理

应用使用StatefulWidget进行状态管理,主要状态变量包括:

  • _selectedIndex:当前选中的标签页索引
  • _currentUVData:当前城市的紫外线数据
  • _citiesUVData:所有城市的紫外线数据列表
  • _uvForecast:紫外线预报数据列表
  • _uvAlerts:紫外线预警信息列表
  • _selectedCity:当前选中的城市
  • _isLoading:数据加载状态

数据流设计

数据层 状态管理 界面层 用户 数据层 状态管理 界面层 用户 选择城市 更新选中城市 获取紫外线数据 返回UV数据 更新界面显示 展示紫外线信息

用户界面实现

主界面布局

主界面采用Scaffold + NavigationBar的布局结构:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('实时紫外线强度查询'),
      backgroundColor: Colors.orange.withValues(alpha: 0.1),
      actions: [
        IconButton(
          onPressed: _loadUVData,
          icon: _isLoading 
              ? const SizedBox(
                  width: 20,
                  height: 20,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              : const Icon(Icons.refresh),
          tooltip: '刷新数据',
        ),
        IconButton(
          onPressed: () => _showSettings(),
          icon: const Icon(Icons.settings),
          tooltip: '设置',
        ),
      ],
    ),
    body: IndexedStack(
      index: _selectedIndex,
      children: [
        _buildCurrentPage(),    // 当前页面
        _buildCitiesPage(),     // 城市页面
        _buildForecastPage(),   // 预报页面
        _buildAlertsPage(),     // 预警页面
      ],
    ),
    bottomNavigationBar: NavigationBar(
      selectedIndex: _selectedIndex,
      onDestinationSelected: (index) {
        setState(() => _selectedIndex = index);
      },
      destinations: const [
        NavigationDestination(icon: Icon(Icons.wb_sunny), label: '当前'),
        NavigationDestination(icon: Icon(Icons.location_city), label: '城市'),
        NavigationDestination(icon: Icon(Icons.calendar_today), label: '预报'),
        NavigationDestination(icon: Icon(Icons.warning), label: '预警'),
      ],
    ),
  );
}

当前紫外线页面设计

当前页面是应用的核心功能模块,展示实时紫外线信息:

城市选择器
Widget _buildCitySelector() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          const Icon(Icons.location_on, color: Colors.orange),
          const SizedBox(width: 12),
          const Text(
            '选择城市:',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: DropdownButtonFormField<String>(
              value: _selectedCity,
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              ),
              items: _cities.map((city) {
                return DropdownMenuItem(value: city, child: Text(city));
              }).toList(),
              onChanged: (value) {
                setState(() => _selectedCity = value!);
                _loadUVData();
              },
            ),
          ),
        ],
      ),
    ),
  );
}
紫外线指数展示
Widget _buildCurrentUVCard() {
  final uvData = _currentUVData!;
  
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(20),
      child: Column(
        children: [
          // 城市名称和更新时间
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                uvData.cityName,
                style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              ),
              Text(
                '${uvData.updateTime.hour.toString().padLeft(2, '0')}:${uvData.updateTime.minute.toString().padLeft(2, '0')} 更新',
                style: TextStyle(fontSize: 14, color: Colors.grey[600]),
              ),
            ],
          ),
          const SizedBox(height: 20),
          // 紫外线指数圆形显示
          Container(
            width: 150,
            height: 150,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: uvData.uvLevelColor.withValues(alpha: 0.1),
              border: Border.all(color: uvData.uvLevelColor, width: 3),
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(uvData.uvLevelIcon, size: 40, color: uvData.uvLevelColor),
                const SizedBox(height: 8),
                Text(
                  uvData.uvIndex.toString(),
                  style: TextStyle(
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                    color: uvData.uvLevelColor,
                  ),
                ),
                Text(
                  uvData.uvLevel,
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w600,
                    color: uvData.uvLevelColor,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 20),
          // 防护等级和建议
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: uvData.uvLevelColor.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(12),
              border: Border.all(color: uvData.uvLevelColor.withValues(alpha: 0.3)),
            ),
            child: Column(
              children: [
                Text(
                  uvData.protectionLevel,
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: uvData.uvLevelColor,
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  uvData.recommendation,
                  style: const TextStyle(fontSize: 14),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}
环境信息展示
Widget _buildWeatherInfo() {
  final uvData = _currentUVData!;
  
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '环境信息',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(
                child: _buildInfoItem(
                  Icons.thermostat, '温度', '${uvData.temperature}°C', Colors.red,
                ),
              ),
              Expanded(
                child: _buildInfoItem(
                  Icons.water_drop, '湿度', '${uvData.humidity}%', Colors.blue,
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: _buildInfoItem(
                  Icons.cloud, '云量', '${(uvData.cloudCover * 100).toInt()}%', Colors.grey,
                ),
              ),
              Expanded(
                child: _buildInfoItem(
                  Icons.air, 'AQI', uvData.aqi.toString(), _getAQIColor(uvData.aqi),
                ),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}
24小时趋势图
Widget _buildHourlyChart() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '24小时紫外线趋势',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          SizedBox(
            height: 100,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: 24,
              itemBuilder: (context, index) {
                final hour = index;
                final random = Random(hour);
                double uvValue;
                
                // 根据时间模拟紫外线强度
                if (hour < 6 || hour > 18) {
                  uvValue = random.nextDouble() * 1.0;
                } else if (hour >= 10 && hour <= 14) {
                  uvValue = 6.0 + random.nextDouble() * 5.0;
                } else {
                  uvValue = 2.0 + random.nextDouble() * 4.0;
                }
                
                return Container(
                  width: 50,
                  margin: const EdgeInsets.only(right: 8),
                  child: Column(
                    children: [
                      Text(
                        uvValue.toStringAsFixed(1),
                        style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 4),
                      Expanded(
                        child: Container(
                          width: 20,
                          decoration: BoxDecoration(
                            color: _getUVColor(uvValue),
                            borderRadius: BorderRadius.circular(10),
                          ),
                          alignment: Alignment.bottomCenter,
                          child: FractionallySizedBox(
                            heightFactor: (uvValue / 12).clamp(0.1, 1.0),
                            child: Container(
                              decoration: BoxDecoration(
                                color: _getUVColor(uvValue),
                                borderRadius: BorderRadius.circular(10),
                              ),
                            ),
                          ),
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        '${hour.toString().padLeft(2, '0')}:00',
                        style: TextStyle(fontSize: 10, color: Colors.grey[600]),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    ),
  );
}

城市页面设计

城市页面展示全国各大城市的紫外线指数:

Widget _buildCitiesPage() {
  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '全国城市紫外线指数',
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        Text(
          '实时更新各大城市紫外线强度',
          style: TextStyle(fontSize: 14, color: Colors.grey[600]),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: ListView.builder(
            itemCount: _citiesUVData.length,
            itemBuilder: (context, index) {
              final cityData = _citiesUVData[index];
              return _buildCityCard(cityData);
            },
          ),
        ),
      ],
    ),
  );
}

预报页面设计

预报页面展示未来7天的紫外线强度预测:

Widget _buildForecastPage() {
  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '$_selectedCity 7天紫外线预报',
          style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        Text(
          '未来一周紫外线强度趋势',
          style: TextStyle(fontSize: 14, color: Colors.grey[600]),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: ListView.builder(
            itemCount: _uvForecast.length,
            itemBuilder: (context, index) {
              final forecast = _uvForecast[index];
              return _buildForecastCard(forecast, index == 0);
            },
          ),
        ),
      ],
    ),
  );
}

预警页面设计

预警页面展示紫外线预警信息:

Widget _buildAlertsPage() {
  return Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '紫外线预警信息',
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        Text(
          '及时了解紫外线预警,做好防护准备',
          style: TextStyle(fontSize: 14, color: Colors.grey[600]),
        ),
        const SizedBox(height: 16),
        Expanded(
          child: _uvAlerts.isEmpty
              ? const Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.check_circle, size: 64, color: Colors.green),
                      SizedBox(height: 16),
                      Text(
                        '暂无紫外线预警',
                        style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
                      ),
                      SizedBox(height: 8),
                      Text(
                        '当前紫外线强度正常,请继续关注',
                        style: TextStyle(fontSize: 14, color: Colors.grey),
                      ),
                    ],
                  ),
                )
              : ListView.builder(
                  itemCount: _uvAlerts.length,
                  itemBuilder: (context, index) {
                    final alert = _uvAlerts[index];
                    return _buildAlertCard(alert);
                  },
                ),
        ),
      ],
    ),
  );
}

核心功能实现

紫外线数据生成

应用内置了智能的紫外线数据生成算法,根据时间和地理位置模拟真实的紫外线强度:

void _generateCurrentUVData() {
  final random = Random();
  final now = DateTime.now();
  final hour = now.hour;
  
  // 根据时间模拟紫外线强度
  double baseUV;
  if (hour < 6 || hour > 18) {
    baseUV = 0.0 + random.nextDouble() * 1.0;      // 夜间和早晚
  } else if (hour >= 10 && hour <= 14) {
    baseUV = 6.0 + random.nextDouble() * 5.0;      // 中午时段
  } else {
    baseUV = 2.0 + random.nextDouble() * 4.0;      // 其他时段
  }

  final uvIndex = double.parse(baseUV.toStringAsFixed(1));
  
  _currentUVData = UVData(
    cityId: 'city_001',
    cityName: _selectedCity,
    uvIndex: uvIndex,
    uvLevel: _getUVLevel(uvIndex),
    updateTime: now,
    latitude: 39.9042 + random.nextDouble() * 0.1,
    longitude: 116.4074 + random.nextDouble() * 0.1,
    weather: _getRandomWeather(),
    temperature: 15 + random.nextInt(20),
    humidity: 40 + random.nextInt(40),
    cloudCover: random.nextDouble(),
    aqi: 50 + random.nextInt(150),
    recommendation: _getRecommendation(uvIndex),
    protectionTips: _getProtectionTips(uvIndex),
  );
}

紫外线等级判定

根据WHO标准判定紫外线等级:

String _getUVLevel(double uvIndex) {
  if (uvIndex <= 2) return '低';          // 0-2: 低
  if (uvIndex <= 5) return '中等';        // 3-5: 中等
  if (uvIndex <= 7) return '高';          // 6-7: 高
  if (uvIndex <= 10) return '很高';       // 8-10: 很高
  return '极高';                          // 11+: 极高
}

防护建议生成

根据紫外线强度提供个性化防护建议:

String _getRecommendation(double uvIndex) {
  if (uvIndex <= 2) return '可以安全地待在户外';
  if (uvIndex <= 5) return '外出时建议戴帽子';
  if (uvIndex <= 7) return '需要防护措施,避免长时间暴露';
  if (uvIndex <= 10) return '必须采取防护措施,减少户外活动';
  return '避免外出,如需外出请做好全面防护';
}

List<String> _getProtectionTips(double uvIndex) {
  if (uvIndex <= 2) {
    return ['可以正常户外活动', '无需特殊防护'];
  } else if (uvIndex <= 5) {
    return ['戴帽子和太阳镜', '使用SPF15+防晒霜', '寻找阴凉处'];
  } else if (uvIndex <= 7) {
    return ['使用SPF30+防晒霜', '穿长袖衣物', '戴宽边帽', '避免10-16点外出'];
  } else if (uvIndex <= 10) {
    return ['使用SPF50+防晒霜', '穿防护服装', '戴太阳镜', '尽量待在室内'];
  } else {
    return ['避免外出', '如需外出全身防护', '使用SPF50+防晒霜', '穿长袖长裤'];
  }
}

城市数据管理

为全国主要城市生成紫外线数据:

void _generateCitiesUVData() {
  final random = Random();
  _citiesUVData.clear();
  
  for (String city in _cities) {
    final uvIndex = random.nextDouble() * 12;
    _citiesUVData.add(UVData(
      cityId: 'city_${_cities.indexOf(city)}',
      cityName: city,
      uvIndex: double.parse(uvIndex.toStringAsFixed(1)),
      uvLevel: _getUVLevel(uvIndex),
      updateTime: DateTime.now(),
      latitude: 30.0 + random.nextDouble() * 20,
      longitude: 100.0 + random.nextDouble() * 30,
      weather: _getRandomWeather(),
      temperature: 10 + random.nextInt(25),
      humidity: 30 + random.nextInt(50),
      cloudCover: random.nextDouble(),
      aqi: 30 + random.nextInt(200),
      recommendation: _getRecommendation(uvIndex),
      protectionTips: _getProtectionTips(uvIndex),
    ));
  }
}

交互功能设计

城市详情展示

点击城市卡片时,显示详细的紫外线信息:

void _showCityDetail(UVData cityData) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text('${cityData.cityName} 紫外线详情'),
      content: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            // 紫外线指数展示
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: cityData.uvLevelColor.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: cityData.uvLevelColor.withValues(alpha: 0.3)),
              ),
              child: Column(
                children: [
                  Icon(cityData.uvLevelIcon, size: 40, color: cityData.uvLevelColor),
                  const SizedBox(height: 8),
                  Text(
                    '紫外线指数 ${cityData.uvIndex}',
                    style: TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                      color: cityData.uvLevelColor,
                    ),
                  ),
                  Text(
                    cityData.uvLevel,
                    style: TextStyle(fontSize: 16, color: cityData.uvLevelColor),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 16),
            // 环境信息
            Text('天气:${cityData.weather}'),
            Text('温度:${cityData.temperature}°C'),
            Text('湿度:${cityData.humidity}%'),
            Text('空气质量指数:${cityData.aqi}'),
            const SizedBox(height: 16),
            // 防护建议
            Text('防护建议:', style: TextStyle(fontWeight: FontWeight.bold)),
            Text(cityData.recommendation),
            const SizedBox(height: 16),
            // 防护措施
            Text('防护措施:', style: TextStyle(fontWeight: FontWeight.bold)),
            ...cityData.protectionTips.map((tip) => Text('• $tip')),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('关闭'),
        ),
        ElevatedButton(
          onPressed: () {
            setState(() => _selectedCity = cityData.cityName);
            Navigator.pop(context);
            _loadUVData();
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('已切换到${cityData.cityName}')),
            );
          },
          child: const Text('切换到此城市'),
        ),
      ],
    ),
  );
}

预警详情展示

显示紫外线预警的详细信息:

void _showAlertDetail(UVAlert alert) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Row(
        children: [
          Icon(alert.alertIcon, color: alert.alertColor),
          const SizedBox(width: 8),
          Expanded(child: Text(alert.title)),
        ],
      ),
      content: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            // 预警等级
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: alert.alertColor.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: alert.alertColor.withValues(alpha: 0.3)),
              ),
              child: Text(
                alert.level,
                style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  color: alert.alertColor,
                ),
                textAlign: TextAlign.center,
              ),
            ),
            const SizedBox(height: 16),
            // 预警内容
            Text('预警内容:', style: TextStyle(fontWeight: FontWeight.bold)),
            Text(alert.message),
            const SizedBox(height: 16),
            // 时间信息
            Text('发布时间:', style: TextStyle(fontWeight: FontWeight.bold)),
            Text('${alert.issueTime.year}${alert.issueTime.month}${alert.issueTime.day}日 ${alert.issueTime.hour.toString().padLeft(2, '0')}:${alert.issueTime.minute.toString().padLeft(2, '0')}'),
            const SizedBox(height: 16),
            Text('有效期至:', style: TextStyle(fontWeight: FontWeight.bold)),
            Text('${alert.expireTime.year}${alert.expireTime.month}${alert.expireTime.day}日 ${alert.expireTime.hour.toString().padLeft(2, '0')}:${alert.expireTime.minute.toString().padLeft(2, '0')}'),
            const SizedBox(height: 16),
            // 影响区域
            Text('影响区域:', style: TextStyle(fontWeight: FontWeight.bold)),
            Text(alert.affectedAreas.join('、')),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('关闭'),
        ),
        ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('预警已收藏')),
            );
          },
          child: const Text('收藏'),
        ),
      ],
    ),
  );
}

数据可视化

紫外线强度色彩映射

根据WHO标准为不同紫外线强度设置对应颜色:

Color _getUVColor(double uvIndex) {
  if (uvIndex <= 2) return Colors.green;      // 低:绿色
  if (uvIndex <= 5) return Colors.yellow;     // 中等:黄色
  if (uvIndex <= 7) return Colors.orange;     // 高:橙色
  if (uvIndex <= 10) return Colors.red;       // 很高:红色
  return Colors.purple;                       // 极高:紫色
}

空气质量指数色彩

为空气质量指数设置对应颜色:

Color _getAQIColor(int aqi) {
  if (aqi <= 50) return Colors.green;         // 优
  if (aqi <= 100) return Colors.yellow;       // 良
  if (aqi <= 150) return Colors.orange;       // 轻度污染
  if (aqi <= 200) return Colors.red;          // 中度污染
  return Colors.purple;                       // 重度污染
}

24小时趋势图

使用柱状图展示24小时紫外线强度变化:

Widget _buildHourlyChart() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '24小时紫外线趋势',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          SizedBox(
            height: 100,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: 24,
              itemBuilder: (context, index) {
                final hour = index;
                final uvValue = _calculateHourlyUV(hour);
                
                return Container(
                  width: 50,
                  margin: const EdgeInsets.only(right: 8),
                  child: Column(
                    children: [
                      Text(
                        uvValue.toStringAsFixed(1),
                        style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 4),
                      Expanded(
                        child: Container(
                          width: 20,
                          alignment: Alignment.bottomCenter,
                          child: FractionallySizedBox(
                            heightFactor: (uvValue / 12).clamp(0.1, 1.0),
                            child: Container(
                              decoration: BoxDecoration(
                                color: _getUVColor(uvValue),
                                borderRadius: BorderRadius.circular(10),
                              ),
                            ),
                          ),
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        '${hour.toString().padLeft(2, '0')}:00',
                        style: TextStyle(fontSize: 10, color: Colors.grey[600]),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    ),
  );
}

性能优化策略

内存管理

  1. 数据结构优化:使用final修饰符减少不必要的重建
  2. 列表优化:使用ListView.builder进行懒加载
  3. 状态管理:合理使用setState,避免全局重建

渲染优化

  1. Widget复用:提取公共组件,减少重复构建
  2. 常量使用:使用const构造函数优化性能
  3. 条件渲染:避免不必要的Widget构建

代码优化示例

// 使用const优化性能
const Text(
  '实时紫外线强度查询',
  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
)

// 使用ListView.builder进行懒加载
ListView.builder(
  itemCount: _citiesUVData.length,
  itemBuilder: (context, index) {
    final cityData = _citiesUVData[index];
    return _buildCityCard(cityData);
  },
)

// 提取公共组件
Widget _buildInfoItem(IconData icon, String label, String value, Color color) {
  return Container(
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: color.withValues(alpha: 0.1),
      borderRadius: BorderRadius.circular(8),
    ),
    child: Column(
      children: [
        Icon(icon, color: color, size: 24),
        Text(value, style: TextStyle(fontWeight: FontWeight.bold, color: color)),
        Text(label, style: TextStyle(color: Colors.grey[600])),
      ],
    ),
  );
}

测试与调试

单元测试

为核心功能编写单元测试:

import 'package:flutter_test/flutter_test.dart';
import 'package:uv_index_app/main.dart';

void main() {
  group('UVData Tests', () {
    test('should create UV data with correct properties', () {
      final uvData = UVData(
        cityId: 'test_001',
        cityName: '测试城市',
        uvIndex: 6.5,
        uvLevel: '高',
        updateTime: DateTime.now(),
        latitude: 39.9042,
        longitude: 116.4074,
        weather: '晴天',
        temperature: 25,
        humidity: 60,
        cloudCover: 0.2,
        aqi: 80,
        recommendation: '需要防护措施',
        protectionTips: ['使用防晒霜', '戴帽子'],
      );

      expect(uvData.cityName, '测试城市');
      expect(uvData.uvIndex, 6.5);
      expect(uvData.uvLevelColor, Colors.orange);
      expect(uvData.protectionLevel, '中度防护');
    });

    test('should calculate UV level correctly', () {
      expect(_getUVLevel(1.5), '低');
      expect(_getUVLevel(4.0), '中等');
      expect(_getUVLevel(6.5), '高');
      expect(_getUVLevel(9.0), '很高');
      expect(_getUVLevel(12.0), '极高');
    });
  });

  group('UVForecast Tests', () {
    test('should create forecast with correct color', () {
      final forecast = UVForecast(
        date: DateTime.now(),
        maxUV: 8.5,
        minUV: 2.0,
        weather: '多云',
        maxTemp: 28,
        minTemp: 18,
      );

      expect(forecast.maxUVColor, Colors.red);
    });
  });
}

集成测试

测试应用的整体功能流程:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:uv_index_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('UV Index App Integration Tests', () {
    testWidgets('should navigate between tabs', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 测试标签页切换
      await tester.tap(find.text('城市'));
      await tester.pumpAndSettle();
      expect(find.text('全国城市紫外线指数'), findsOneWidget);

      await tester.tap(find.text('预报'));
      await tester.pumpAndSettle();
      expect(find.text('7天紫外线预报'), findsOneWidget);

      await tester.tap(find.text('预警'));
      await tester.pumpAndSettle();
      expect(find.text('紫外线预警信息'), findsOneWidget);
    });

    testWidgets('should change city and refresh data', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 等待数据加载完成
      await tester.pump(Duration(seconds: 3));

      // 测试城市切换
      await tester.tap(find.text('北京').first);
      await tester.pumpAndSettle();
      await tester.tap(find.text('上海'));
      await tester.pumpAndSettle();

      // 验证城市切换成功
      expect(find.text('上海'), findsWidgets);
    });

    testWidgets('should show city detail dialog', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 切换到城市页面
      await tester.tap(find.text('城市'));
      await tester.pumpAndSettle();

      // 等待数据加载
      await tester.pump(Duration(seconds: 3));

      // 点击第一个城市卡片
      await tester.tap(find.byType(Card).first);
      await tester.pumpAndSettle();

      // 验证详情对话框显示
      expect(find.byType(AlertDialog), findsOneWidget);
      expect(find.text('紫外线详情'), findsOneWidget);
    });
  });
}

部署与发布

Android平台部署

1. 配置应用信息

编辑android/app/build.gradle

android {
    compileSdkVersion 33
    
    defaultConfig {
        applicationId "com.example.uv_index_app"
        minSdkVersion 21
        targetSdkVersion 33
        versionCode 1
        versionName "1.0.0"
    }
    
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
2. 权限配置

android/app/src/main/AndroidManifest.xml中添加必要权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
3. 构建发布版本
flutter build apk --release

iOS平台部署

1. 配置Xcode项目

在Xcode中打开ios/Runner.xcworkspace,配置:

  • Bundle Identifier
  • Team设置
  • 版本信息
  • 位置权限描述
2. 权限配置

ios/Runner/Info.plist中添加位置权限描述:

<key>NSLocationWhenInUseUsageDescription</key>
<string>此应用需要访问您的位置以提供准确的紫外线信息</string>
3. 构建发布版本
flutter build ios --release

扩展功能设计

定位服务集成

实现自动获取用户位置功能:

class LocationService {
  static Future<Position?> getCurrentLocation() async {
    try {
      bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
      if (!serviceEnabled) {
        return null;
      }

      LocationPermission permission = await Geolocator.checkPermission();
      if (permission == LocationPermission.denied) {
        permission = await Geolocator.requestPermission();
        if (permission == LocationPermission.denied) {
          return null;
        }
      }

      return await Geolocator.getCurrentPosition();
    } catch (e) {
      print('获取位置失败: $e');
      return null;
    }
  }

  static String getCityFromCoordinates(double lat, double lng) {
    // 根据经纬度获取城市名称的逻辑
    return '当前位置';
  }
}

推送通知服务

实现紫外线预警推送:

class NotificationService {
  static Future<void> scheduleUVAlert({
    required String title,
    required String body,
    required DateTime scheduledDate,
  }) async {
    // 实现推送通知逻辑
    print('推送紫外线预警:$title - $body');
  }

  static Future<void> checkAndSendUVAlert(double uvIndex) async {
    if (uvIndex >= 8.0) {
      await scheduleUVAlert(
        title: '紫外线强度预警',
        body: '当前紫外线指数${uvIndex.toStringAsFixed(1)},请做好防护措施!',
        scheduledDate: DateTime.now().add(Duration(minutes: 1)),
      );
    }
  }
}

个人防护记录

实现用户防护记录功能:

class ProtectionRecord {
  final String id;
  final DateTime date;
  final double uvIndex;
  final List<String> protectionMeasures;
  final String skinCondition;
  final String notes;

  ProtectionRecord({
    required this.id,
    required this.date,
    required this.uvIndex,
    required this.protectionMeasures,
    required this.skinCondition,
    required this.notes,
  });
}

class ProtectionService {
  static final List<ProtectionRecord> _records = [];

  static void addRecord(ProtectionRecord record) {
    _records.add(record);
  }

  static List<ProtectionRecord> getRecords() {
    return List.from(_records);
  }

  static Map<String, int> getProtectionStats() {
    final stats = <String, int>{};
    for (var record in _records) {
      for (var measure in record.protectionMeasures) {
        stats[measure] = (stats[measure] ?? 0) + 1;
      }
    }
    return stats;
  }
}

皮肤类型评估

根据用户皮肤类型提供个性化建议:

enum SkinType {
  type1, // 极易晒伤,从不晒黑
  type2, // 易晒伤,难晒黑
  type3, // 有时晒伤,逐渐晒黑
  type4, // 很少晒伤,容易晒黑
  type5, // 极少晒伤,很容易晒黑
  type6, // 从不晒伤,深色皮肤
}

class SkinTypeService {
  static List<String> getProtectionTips(SkinType skinType, double uvIndex) {
    final baseTips = _getBaseTips(uvIndex);
    final skinSpecificTips = _getSkinSpecificTips(skinType, uvIndex);
    
    return [...baseTips, ...skinSpecificTips];
  }

  static List<String> _getBaseTips(double uvIndex) {
    // 基础防护建议
    if (uvIndex <= 2) return ['可以正常户外活动'];
    if (uvIndex <= 5) return ['使用SPF15+防晒霜', '戴帽子'];
    if (uvIndex <= 7) return ['使用SPF30+防晒霜', '穿长袖', '戴帽子'];
    if (uvIndex <= 10) return ['使用SPF50+防晒霜', '穿防护服', '避免户外'];
    return ['避免外出', '全面防护'];
  }

  static List<String> _getSkinSpecificTips(SkinType skinType, double uvIndex) {
    switch (skinType) {
      case SkinType.type1:
      case SkinType.type2:
        return uvIndex > 3 ? ['特别注意防护', '考虑物理防晒'] : [];
      case SkinType.type3:
      case SkinType.type4:
        return uvIndex > 6 ? ['加强防护措施'] : [];
      case SkinType.type5:
      case SkinType.type6:
        return uvIndex > 8 ? ['注意长时间暴露'] : [];
    }
  }
}

用户体验优化

无障碍支持

为应用添加无障碍功能:

Semantics(
  label: '紫外线指数${uvData.uvIndex},等级${uvData.uvLevel}${uvData.recommendation}',
  hint: '点击查看详细信息',
  child: _buildCurrentUVCard(),
)

国际化支持

支持多语言界面:

// 在pubspec.yaml中添加
dependencies:
  flutter_localizations:
    sdk: flutter

// 配置本地化
MaterialApp(
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('zh', 'CN'),
    Locale('en', 'US'),
  ],
)

主题定制

提供多种主题选择:

class ThemeService {
  static ThemeData get lightTheme => ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.orange,
      brightness: Brightness.light,
    ),
    useMaterial3: true,
  );

  static ThemeData get darkTheme => ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.orange,
      brightness: Brightness.dark,
    ),
    useMaterial3: true,
  );

  static ThemeData get sunTheme => ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.amber,
      brightness: Brightness.light,
    ),
    useMaterial3: true,
  );
}

总结与展望

项目成果

本项目成功开发了一款功能完整的Flutter实时紫外线强度查询应用,实现了以下核心功能:

  1. 实时紫外线监测:准确显示当前紫外线指数和等级
  2. 全国城市覆盖:支持16个主要城市的紫外线查询
  3. 7天预报功能:提供未来一周的紫外线趋势预测
  4. 智能防护建议:根据紫外线强度提供个性化防护指导
  5. 预警提醒系统:及时发出紫外线强度异常预警
  6. 24小时趋势图:直观展示一天内紫外线变化

技术亮点

  • 跨平台兼容:基于Flutter框架,支持多平台部署
  • 响应式设计:适配不同屏幕尺寸和设备类型
  • 数据可视化:丰富的图表和色彩映射
  • 性能优化:采用多种优化策略,确保流畅体验
  • 科学准确:基于WHO标准的紫外线等级判定

应用价值

  1. 健康防护:科学指导用户进行紫外线防护
  2. 出行规划:帮助用户合理安排户外活动
  3. 预警保护:及时提醒紫外线强度异常
  4. 教育意义:提高用户对紫外线危害的认识

未来发展方向

  1. AI智能推荐:基于用户行为和皮肤类型的个性化建议
  2. 实时API集成:接入真实的紫外线数据API
  3. 社交功能:用户分享防护经验和心得
  4. 健康档案:长期跟踪用户的防护记录和皮肤状况
  5. 可穿戴设备集成:与智能手表等设备联动

学习价值

通过本项目的开发,开发者可以掌握:

  • Flutter跨平台开发技术
  • 数据可视化和图表绘制
  • 状态管理和性能优化
  • 用户体验设计原则
  • 健康类应用开发经验

本项目不仅是一个实用的紫外线查询工具,更是Flutter开发技术的综合实践,为用户的健康防护提供了科学指导,同时也为移动应用开发者提供了宝贵的学习资源。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐