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



1. 项目介绍
随着城市化进程的加速和环境问题的日益突出,空气质量已经成为人们关注的焦点。了解所在城市的空气质量状况,对于人们的健康生活和出行规划具有重要意义。城市空气质量简易指数查询卡片是一个基于 Flutter 开发的应用,它能够实时查询和显示城市的空气质量指数,帮助用户了解空气质量状况,为出行和健康管理提供参考。本文将详细介绍如何使用 Flutter 实现这个实用的空气质量查询应用。
1.1 项目目标
- 实现一个查询城市空气质量指数的应用
- 支持多个城市的空气质量数据查询
- 显示空气质量指数、等级和主要污染物
- 提供污染物详情和空气质量说明
- 采用简洁美观的界面设计
- 确保在不同平台上的一致性表现
1.2 技术栈
- Flutter:跨平台 UI 框架
- Dart:编程语言
- StatefulWidget:用于管理应用状态
- DropdownButtonFormField:用于城市选择
- GridView.builder:用于污染物详情的网格布局
- LinearGradient:用于渐变背景
- DateFormat:用于格式化日期时间
2. 核心功能设计
2.1 城市选择
- 城市列表:支持从20个主要城市中选择
- 下拉选择:使用下拉菜单选择城市
- 实时更新:选择城市后实时更新空气质量数据
2.2 空气质量指数显示
- AQI 指数:显示当前城市的空气质量指数
- 空气质量等级:根据AQI指数显示对应的空气质量等级
- 等级颜色:使用不同颜色表示不同的空气质量等级
- 空气质量描述:提供空气质量状况的文字描述
2.3 污染物详情
- 污染物列表:显示PM2.5、PM10、SO2、NO2、O3、CO等污染物的浓度
- 网格布局:使用网格布局展示污染物详情
- 主要污染物:显示当前城市的主要污染物
2.4 空气质量说明
- AQI 说明:提供空气质量指数的详细说明
- 等级说明:展示不同空气质量等级的划分标准
- 颜色编码:使用颜色编码表示不同的空气质量等级
2.5 界面设计
- 渐变背景:使用蓝色渐变背景,简洁美观
- 卡片式布局:空气质量数据以卡片形式展示,层次分明
- 响应式设计:适应不同屏幕尺寸
- 加载动画:数据加载时显示加载动画,提升用户体验
3. 技术架构
3.1 项目结构
lib/
└── main.dart # 主应用文件,包含所有代码
3.2 组件结构
AirQualityApp
└── AirQualityScreen
├── State management (selectedCity, airQualityData, isLoading)
├── Data fetching (_fetchAirQualityData, _getMockAirQualityData)
├── UI components
│ ├── City selection
│ ├── Air quality card
│ │ ├── City and update time
│ │ ├── AQI and level
│ │ ├── Description
│ │ ├── Primary pollutant
│ │ └── Pollutant details
│ └── AQI explanation
└── Helper methods (_buildAqiLevelItem)
3.3 数据模型
- AirQualityData:空气质量数据模型,包含城市、AQI指数、空气质量等级、颜色、描述、主要污染物、污染物详情和更新时间
- 城市列表:包含20个主要城市的列表
- 污染物数据:包含6种主要污染物的浓度数据
4. 关键代码解析
4.1 空气质量数据模型
dart
class AirQualityData {
final String city;
final int aqi;
final String level;
final String color;
final String description;
final String primaryPollutant;
final Map<String, String> pollutants;
final DateTime updateTime;
const AirQualityData({
required this.city,
required this.aqi,
required this.level,
required this.color,
required this.description,
required this.primaryPollutant,
required this.pollutants,
required this.updateTime,
});
static AirQualityData get defaultData {
return AirQualityData(
city: '北京',
aqi: 50,
level: '优',
color: '#00E400',
description: '空气质量令人满意,基本无空气污染',
primaryPollutant: '无',
pollutants: {
'PM2.5': '15',
'PM10': '30',
'SO2': '8',
'NO2': '12',
'O3': '40',
'CO': '0.8',
},
updateTime: DateTime.now(),
);
}
}
代码解析:
AirQualityData类:空气质量数据模型,包含所有空气质量相关的信息defaultData方法:返回默认的空气质量数据- 字段说明:
city:城市名称aqi:空气质量指数level:空气质量等级color:空气质量等级对应的颜色description:空气质量描述primaryPollutant:主要污染物pollutants:各种污染物的浓度updateTime:数据更新时间
4.2 数据获取和处理
dart
void _fetchAirQualityData(String city) {
setState(() {
_isLoading = true;
});
// 模拟网络请求
Future.delayed(const Duration(milliseconds: 800), () {
setState(() {
_airQualityData = _getMockAirQualityData(city);
_isLoading = false;
});
});
}
AirQualityData _getMockAirQualityData(String city) {
// 模拟不同城市的空气质量数据
final random = DateTime.now().millisecond;
final aqi = (random % 150) + 20; // 20-170
String level, color, description;
if (aqi <= 50) {
level = '优';
color = '#00E400';
description = '空气质量令人满意,基本无空气污染';
} else if (aqi <= 100) {
level = '良';
color = '#FFFF00';
description = '空气质量可接受,但某些污染物可能对极少数异常敏感人群健康有较弱影响';
} else if (aqi <= 150) {
level = '轻度污染';
color = '#FF7E00';
description = '易感人群症状有轻度加剧,健康人群出现刺激症状';
} else if (aqi <= 200) {
level = '中度污染';
color = '#FF0000';
description = '进一步加剧易感人群症状,可能对健康人群心脏、呼吸系统有影响';
} else if (aqi <= 300) {
level = '重度污染';
color = '#99004C';
description = '心脏病和肺病患者症状显著加剧,运动耐受力降低,健康人群普遍出现症状';
} else {
level = '严重污染';
color = '#7E0023';
description = '健康人群运动耐受力降低,有明显强烈症状,提前出现某些疾病';
}
final pollutants = {
'PM2.5': ((random % 75) + 5).toString(),
'PM10': ((random % 100) + 10).toString(),
'SO2': ((random % 50) + 5).toString(),
'NO2': ((random % 60) + 5).toString(),
'O3': ((random % 160) + 10).toString(),
'CO': ((random % 40) / 10 + 0.5).toStringAsFixed(1),
};
// 找出主要污染物
String primaryPollutant = '无';
int maxValue = 0;
pollutants.forEach((key, value) {
int intValue = int.tryParse(value) ?? 0;
if (intValue > maxValue) {
maxValue = intValue;
primaryPollutant = key;
}
});
return AirQualityData(
city: city,
aqi: aqi,
level: level,
color: color,
description: description,
primaryPollutant: primaryPollutant,
pollutants: pollutants,
updateTime: DateTime.now(),
);
}
代码解析:
_fetchAirQualityData方法:模拟网络请求,获取空气质量数据_getMockAirQualityData方法:生成模拟的空气质量数据- 空气质量等级判断:根据AQI指数判断空气质量等级
- 污染物数据生成:生成各种污染物的浓度数据
- 主要污染物判断:找出浓度最高的污染物作为主要污染物
4.3 城市选择
dart
// 城市选择
Container(
margin: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选择城市',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonFormField<String>(
value: _selectedCity,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedCity = newValue;
_fetchAirQualityData(newValue);
});
}
},
items: _cities.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
),
),
),
],
),
),
代码解析:
DropdownButtonFormField:用于城市选择的下拉菜单onChanged:当选择城市变化时,更新城市并获取新的空气质量数据items:城市列表,用于生成下拉菜单选项decoration:设置下拉菜单的样式
4.4 空气质量卡片
dart
// 空气质量卡片
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(20),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 城市和更新时间
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_airQualityData.city,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'更新时间: ${DateFormat('yyyy-MM-dd HH:mm').format(_airQualityData.updateTime)}',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 20),
// AQI 指数和等级
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'空气质量指数',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 8),
Text(
_airQualityData.aqi.toString(),
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Color(int.parse(_airQualityData.color.replaceAll('#', '0xFF'))),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_airQualityData.level,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 16),
// 空气质量描述
Text(
_airQualityData.description,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
height: 1.4,
),
),
const SizedBox(height: 16),
// 主要污染物
if (_airQualityData.primaryPollutant != '无')
Text(
'主要污染物: ${_airQualityData.primaryPollutant}',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 24),
// 污染物详情
const Text(
'污染物详情',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 2,
),
itemCount: _airQualityData.pollutants.length,
itemBuilder: (context, index) {
final pollutant = _airQualityData.pollutants.entries.elementAt(index);
return Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
pollutant.key,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
pollutant.value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
],
),
),
代码解析:
- 空气质量卡片:显示城市、更新时间、AQI指数、空气质量等级、描述、主要污染物和污染物详情
- 加载动画:数据加载时显示CircularProgressIndicator
- 城市和更新时间:显示当前城市和数据更新时间
- AQI指数和等级:显示空气质量指数和对应的等级,等级使用不同颜色表示
- 空气质量描述:显示空气质量状况的文字描述
- 主要污染物:显示当前城市的主要污染物
- 污染物详情:使用GridView.builder显示各种污染物的浓度
4.5 空气质量指数说明
dart
// 空气质量指数说明
Container(
margin: const EdgeInsets.only(top: 24),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'空气质量指数说明',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const Text(
'空气质量指数(AQI)是描述空气质量状况的综合指数,由PM2.5、PM10、SO2、NO2、O3、CO等六项污染物的浓度计算得出。',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
height: 1.4,
),
),
const SizedBox(height: 12),
Row(
children: [
_buildAqiLevelItem('优', '#00E400'),
const SizedBox(width: 12),
_buildAqiLevelItem('良', '#FFFF00'),
const SizedBox(width: 12),
_buildAqiLevelItem('轻度污染', '#FF7E00'),
],
),
const SizedBox(height: 8),
Row(
children: [
_buildAqiLevelItem('中度污染', '#FF0000'),
const SizedBox(width: 12),
_buildAqiLevelItem('重度污染', '#99004C'),
const SizedBox(width: 12),
_buildAqiLevelItem('严重污染', '#7E0023'),
],
),
],
),
),
Widget _buildAqiLevelItem(String level, String color) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Color(int.parse(color.replaceAll('#', '0xFF'))),
borderRadius: BorderRadius.circular(4),
),
child: Text(
level,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
);
}
代码解析:
- 空气质量指数说明:提供空气质量指数的详细说明和不同等级的划分标准
_buildAqiLevelItem方法:构建空气质量等级的显示项,使用不同颜色表示不同等级- 等级颜色编码:使用颜色编码表示不同的空气质量等级,方便用户直观理解
5. 技术亮点与创新
5.1 数据模拟与处理
- 模拟数据生成:使用随机数生成模拟的空气质量数据,确保应用可以在没有网络连接的情况下正常展示
- 空气质量等级判断:根据AQI指数准确判断空气质量等级,提供相应的颜色和描述
- 主要污染物识别:自动识别浓度最高的污染物作为主要污染物,提供更有针对性的信息
5.2 界面设计
- 渐变背景:使用蓝色渐变背景,营造清新、专业的视觉效果
- 卡片式布局:采用卡片式布局,层次分明,信息展示清晰
- 响应式设计:适应不同屏幕尺寸,确保在各种设备上都能良好显示
- 加载动画:添加加载动画,提升用户体验,减少等待感
5.3 用户交互
- 城市选择:提供20个主要城市的选择,满足不同用户的需求
- 实时更新:选择城市后实时更新空气质量数据,提供即时反馈
- 信息展示:清晰展示空气质量指数、等级、描述、主要污染物和污染物详情
- 空气质量说明:提供详细的空气质量指数说明,帮助用户理解空气质量数据
5.4 技术实现
- 状态管理:使用StatefulWidget管理应用状态,确保数据更新时界面能够及时响应
- 网格布局:使用GridView.builder实现污染物详情的网格布局,展示清晰,节省空间
- 颜色处理:使用Color类处理空气质量等级的颜色,确保颜色显示准确
- 日期格式化:使用DateFormat格式化更新时间,确保时间显示清晰易读
6. 应用场景与扩展
6.1 应用场景
- 日常出行:用户可以在出门前查询空气质量,决定是否需要佩戴口罩
- 健康管理:关注空气质量对健康的影响,特别是对老人、儿童和呼吸道疾病患者
- 旅游规划:在旅游前查询目的地的空气质量,做好相应的准备
- 环境监测:通过长期观察空气质量变化,了解环境状况的趋势
- 教育学习:了解空气质量指数的计算方法和不同污染物的影响
6.2 扩展方向
- 实时数据接入:接入真实的空气质量API,获取实时的空气质量数据
- 地理位置服务:自动获取用户所在位置,显示当地的空气质量
- 历史数据查询:提供历史空气质量数据查询,了解空气质量的变化趋势
- 空气质量预报:提供未来几天的空气质量预报,帮助用户提前规划
- 健康建议:根据空气质量状况,提供相应的健康建议
- 分享功能:支持将空气质量信息分享到社交媒体
- 多语言支持:添加多语言支持,扩大应用的适用范围
- 深色模式:支持深色模式,适应不同的使用环境
7. 代码优化建议
7.1 性能优化
- 使用 const 构造函数:对于不变的Widget,使用const构造函数,减少不必要的重建
- 优化状态管理:对于更复杂的应用,可以使用Provider、Riverpod等状态管理库
- 使用 RepaintBoundary:对于频繁更新的部分,使用RepaintBoundary包裹,减少不必要的重绘
- 延迟加载:对于非关键信息,可以使用延迟加载,提高应用启动速度
7.2 代码结构优化
- 组件化:将UI组件拆分为更小的、可复用的组件,如城市选择器、空气质量卡片等
- 逻辑分离:将业务逻辑与UI逻辑分离,提高代码的可维护性
- 参数化:将颜色、字体大小等参数提取为可配置的常量,便于统一管理
- 错误处理:添加适当的错误处理,提高应用的稳定性
7.3 用户体验优化
- 添加动画效果:添加城市切换、数据更新的动画效果,提升用户体验
- 触觉反馈:在支持的设备上,添加触觉反馈,增强交互体验
- 无障碍支持:添加无障碍支持,提高应用的可访问性
- 网络错误处理:如果接入真实API,添加网络错误处理,提供友好的错误提示
7.4 功能优化
- 实时数据接入:接入真实的空气质量API,提供更准确的空气质量数据
- 地理位置服务:自动获取用户所在位置,显示当地的空气质量
- 历史数据查询:提供历史空气质量数据查询,了解空气质量的变化趋势
- 空气质量预报:提供未来几天的空气质量预报,帮助用户提前规划
8. 测试与调试
8.1 测试策略
- 功能测试:测试城市选择、数据加载、空气质量显示等核心功能
- 性能测试:测试应用在不同设备上的性能表现
- 兼容性测试:测试在不同平台、不同屏幕尺寸上的表现
- 用户体验测试:测试应用的易用性和用户体验
8.2 调试技巧
- 使用 Flutter DevTools:利用Flutter DevTools分析性能瓶颈和调试问题
- 添加日志:在关键位置添加日志,便于调试
- 使用模拟器:在不同尺寸的模拟器上测试,确保适配性
- 用户测试:邀请用户测试,收集反馈,不断改进
9. 总结与展望
9.1 项目总结
本项目成功实现了一个美观实用的城市空气质量简易指数查询卡片应用,主要功能包括:
- 支持20个主要城市的空气质量查询
- 显示空气质量指数、等级和主要污染物
- 提供污染物详情和空气质量说明
- 采用简洁美观的界面设计
- 支持响应式布局,适应不同屏幕尺寸
9.2 技术价值
- 学习价值:展示了如何使用Flutter实现一个完整的空气质量查询应用
- 实用价值:提供了一个可直接使用的空气质量查询工具
- 参考价值:为类似功能的开发提供了参考方案
- 教育价值:有助于理解空气质量指数的计算方法和不同污染物的影响
9.3 未来展望
- 实时数据接入:接入真实的空气质量API,获取实时的空气质量数据
- 地理位置服务:自动获取用户所在位置,显示当地的空气质量
- 历史数据查询:提供历史空气质量数据查询,了解空气质量的变化趋势
- 空气质量预报:提供未来几天的空气质量预报,帮助用户提前规划
- 健康建议:根据空气质量状况,提供相应的健康建议
- 分享功能:支持将空气质量信息分享到社交媒体
- 多语言支持:添加多语言支持,扩大应用的适用范围
- 深色模式:支持深色模式,适应不同的使用环境
10. 附录
10.1 完整代码
dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
void main() {
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
));
runApp(const AirQualityApp());
}
class AirQualityApp extends StatelessWidget {
const AirQualityApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '城市空气质量指数',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const AirQualityScreen(),
);
}
}
class AirQualityScreen extends StatefulWidget {
const AirQualityScreen({Key? key}) : super(key: key);
@override
State<AirQualityScreen> createState() => _AirQualityScreenState();
}
class _AirQualityScreenState extends State<AirQualityScreen> {
String _selectedCity = '北京';
AirQualityData _airQualityData = AirQualityData.defaultData;
bool _isLoading = false;
final List<String> _cities = [
'北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安', '南京', '重庆',
'天津', '苏州', '郑州', '长沙', '沈阳', '青岛', '宁波', '东莞', '厦门', '福州'
];
@override
void initState() {
super.initState();
_fetchAirQualityData(_selectedCity);
}
void _fetchAirQualityData(String city) {
setState(() {
_isLoading = true;
});
// 模拟网络请求
Future.delayed(const Duration(milliseconds: 800), () {
setState(() {
_airQualityData = _getMockAirQualityData(city);
_isLoading = false;
});
});
}
AirQualityData _getMockAirQualityData(String city) {
// 模拟不同城市的空气质量数据
final random = DateTime.now().millisecond;
final aqi = (random % 150) + 20; // 20-170
String level, color, description;
if (aqi <= 50) {
level = '优';
color = '#00E400';
description = '空气质量令人满意,基本无空气污染';
} else if (aqi <= 100) {
level = '良';
color = '#FFFF00';
description = '空气质量可接受,但某些污染物可能对极少数异常敏感人群健康有较弱影响';
} else if (aqi <= 150) {
level = '轻度污染';
color = '#FF7E00';
description = '易感人群症状有轻度加剧,健康人群出现刺激症状';
} else if (aqi <= 200) {
level = '中度污染';
color = '#FF0000';
description = '进一步加剧易感人群症状,可能对健康人群心脏、呼吸系统有影响';
} else if (aqi <= 300) {
level = '重度污染';
color = '#99004C';
description = '心脏病和肺病患者症状显著加剧,运动耐受力降低,健康人群普遍出现症状';
} else {
level = '严重污染';
color = '#7E0023';
description = '健康人群运动耐受力降低,有明显强烈症状,提前出现某些疾病';
}
final pollutants = {
'PM2.5': ((random % 75) + 5).toString(),
'PM10': ((random % 100) + 10).toString(),
'SO2': ((random % 50) + 5).toString(),
'NO2': ((random % 60) + 5).toString(),
'O3': ((random % 160) + 10).toString(),
'CO': ((random % 40) / 10 + 0.5).toStringAsFixed(1),
};
// 找出主要污染物
String primaryPollutant = '无';
int maxValue = 0;
pollutants.forEach((key, value) {
int intValue = int.tryParse(value) ?? 0;
if (intValue > maxValue) {
maxValue = intValue;
primaryPollutant = key;
}
});
return AirQualityData(
city: city,
aqi: aqi,
level: level,
color: color,
description: description,
primaryPollutant: primaryPollutant,
pollutants: pollutants,
updateTime: DateTime.now(),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('城市空气质量指数'),
backgroundColor: Colors.blue.shade800,
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue.shade50,
Colors.white,
],
),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 城市选择
Container(
margin: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选择城市',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonFormField<String>(
value: _selectedCity,
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedCity = newValue;
_fetchAirQualityData(newValue);
});
}
},
items: _cities.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
),
),
),
],
),
),
// 空气质量卡片
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(20),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 城市和更新时间
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_airQualityData.city,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'更新时间: ${DateFormat('yyyy-MM-dd HH:mm').format(_airQualityData.updateTime)}',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 20),
// AQI 指数和等级
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'空气质量指数',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 8),
Text(
_airQualityData.aqi.toString(),
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Color(int.parse(_airQualityData.color.replaceAll('#', '0xFF'))),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_airQualityData.level,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 16),
// 空气质量描述
Text(
_airQualityData.description,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
height: 1.4,
),
),
const SizedBox(height: 16),
// 主要污染物
if (_airQualityData.primaryPollutant != '无')
Text(
'主要污染物: ${_airQualityData.primaryPollutant}',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 24),
// 污染物详情
const Text(
'污染物详情',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 2,
),
itemCount: _airQualityData.pollutants.length,
itemBuilder: (context, index) {
final pollutant = _airQualityData.pollutants.entries.elementAt(index);
return Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
pollutant.key,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
pollutant.value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
],
),
),
// 空气质量指数说明
Container(
margin: const EdgeInsets.only(top: 24),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'空气质量指数说明',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const Text(
'空气质量指数(AQI)是描述空气质量状况的综合指数,由PM2.5、PM10、SO2、NO2、O3、CO等六项污染物的浓度计算得出。',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
height: 1.4,
),
),
const SizedBox(height: 12),
Row(
children: [
_buildAqiLevelItem('优', '#00E400'),
const SizedBox(width: 12),
_buildAqiLevelItem('良', '#FFFF00'),
const SizedBox(width: 12),
_buildAqiLevelItem('轻度污染', '#FF7E00'),
],
),
const SizedBox(height: 8),
Row(
children: [
_buildAqiLevelItem('中度污染', '#FF0000'),
const SizedBox(width: 12),
_buildAqiLevelItem('重度污染', '#99004C'),
const SizedBox(width: 12),
_buildAqiLevelItem('严重污染', '#7E0023'),
],
),
],
),
),
],
),
),
),
);
}
Widget _buildAqiLevelItem(String level, String color) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Color(int.parse(color.replaceAll('#', '0xFF'))),
borderRadius: BorderRadius.circular(4),
),
child: Text(
level,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
);
}
}
class AirQualityData {
final String city;
final int aqi;
final String level;
final String color;
final String description;
final String primaryPollutant;
final Map<String, String> pollutants;
final DateTime updateTime;
const AirQualityData({
required this.city,
required this.aqi,
required this.level,
required this.color,
required this.description,
required this.primaryPollutant,
required this.pollutants,
required this.updateTime,
});
static AirQualityData get defaultData {
return AirQualityData(
city: '北京',
aqi: 50,
level: '优',
color: '#00E400',
description: '空气质量令人满意,基本无空气污染',
primaryPollutant: '无',
pollutants: {
'PM2.5': '15',
'PM10': '30',
'SO2': '8',
'NO2': '12',
'O3': '40',
'CO': '0.8',
},
updateTime: DateTime.now(),
);
}
}
10.2 依赖项
- flutter:Flutter 框架
- flutter/services.dart:提供 SystemChrome 类,用于设置系统 UI 样式
- intl:提供 DateFormat 类,用于格式化日期时间
10.3 运行环境
- Flutter SDK:3.0.0 或更高版本
- Dart SDK:2.17.0 或更高版本
- 支持的平台:Android、iOS、Web、Windows、macOS、Linux