Flutter 框架跨平台鸿蒙开发 - 实时紫外线强度查询:智能防护助手守护健康
实时紫外线监测:准确显示当前紫外线指数和等级全国城市覆盖:支持16个主要城市的紫外线查询7天预报功能:提供未来一周的紫外线趋势预测智能防护建议:根据紫外线强度提供个性化防护指导预警提醒系统:及时发出紫外线强度异常预警24小时趋势图:直观展示一天内紫外线变化。
Flutter实时紫外线强度查询:智能防护助手守护健康
项目概述
随着人们对健康生活的日益重视,紫外线防护已成为日常生活中不可忽视的重要环节。过度的紫外线暴露不仅会导致皮肤晒伤,还可能引发皮肤癌等严重疾病。本项目开发了一款基于Flutter的实时紫外线强度查询应用,旨在为用户提供准确、及时的紫外线指数信息,帮助用户科学防护,守护健康。
运行效果图




核心功能特性
- 实时紫外线监测:提供当前位置的实时紫外线指数和等级
- 全国城市覆盖:支持全国主要城市的紫外线强度查询
- 7天预报功能:未来一周的紫外线强度趋势预测
- 智能防护建议:根据紫外线强度提供个性化防护指导
- 预警提醒系统:紫外线强度异常时及时发出预警
- 24小时趋势图:直观展示一天内紫外线强度变化
- 环境信息集成:结合温度、湿度、空气质量等环境数据
应用价值
- 健康防护:科学指导用户进行紫外线防护,预防皮肤损伤
- 出行规划:帮助用户合理安排户外活动时间
- 个性化建议:根据不同紫外线强度提供针对性防护措施
- 预警保护:及时提醒用户紫外线强度异常情况
开发环境配置
系统要求
开发本应用需要满足以下环境要求:
- 操作系统: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;
}
}
}
应用架构设计
整体架构
应用采用四标签页的架构设计,每个标签页专注于特定功能:
状态管理
应用使用StatefulWidget进行状态管理,主要状态变量包括:
_selectedIndex:当前选中的标签页索引_currentUVData:当前城市的紫外线数据_citiesUVData:所有城市的紫外线数据列表_uvForecast:紫外线预报数据列表_uvAlerts:紫外线预警信息列表_selectedCity:当前选中的城市_isLoading:数据加载状态
数据流设计
用户界面实现
主界面布局
主界面采用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]),
),
],
),
);
},
),
),
],
),
),
);
}
性能优化策略
内存管理
- 数据结构优化:使用final修饰符减少不必要的重建
- 列表优化:使用ListView.builder进行懒加载
- 状态管理:合理使用setState,避免全局重建
渲染优化
- Widget复用:提取公共组件,减少重复构建
- 常量使用:使用const构造函数优化性能
- 条件渲染:避免不必要的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实时紫外线强度查询应用,实现了以下核心功能:
- 实时紫外线监测:准确显示当前紫外线指数和等级
- 全国城市覆盖:支持16个主要城市的紫外线查询
- 7天预报功能:提供未来一周的紫外线趋势预测
- 智能防护建议:根据紫外线强度提供个性化防护指导
- 预警提醒系统:及时发出紫外线强度异常预警
- 24小时趋势图:直观展示一天内紫外线变化
技术亮点
- 跨平台兼容:基于Flutter框架,支持多平台部署
- 响应式设计:适配不同屏幕尺寸和设备类型
- 数据可视化:丰富的图表和色彩映射
- 性能优化:采用多种优化策略,确保流畅体验
- 科学准确:基于WHO标准的紫外线等级判定
应用价值
- 健康防护:科学指导用户进行紫外线防护
- 出行规划:帮助用户合理安排户外活动
- 预警保护:及时提醒紫外线强度异常
- 教育意义:提高用户对紫外线危害的认识
未来发展方向
- AI智能推荐:基于用户行为和皮肤类型的个性化建议
- 实时API集成:接入真实的紫外线数据API
- 社交功能:用户分享防护经验和心得
- 健康档案:长期跟踪用户的防护记录和皮肤状况
- 可穿戴设备集成:与智能手表等设备联动
学习价值
通过本项目的开发,开发者可以掌握:
- Flutter跨平台开发技术
- 数据可视化和图表绘制
- 状态管理和性能优化
- 用户体验设计原则
- 健康类应用开发经验
本项目不仅是一个实用的紫外线查询工具,更是Flutter开发技术的综合实践,为用户的健康防护提供了科学指导,同时也为移动应用开发者提供了宝贵的学习资源。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)