Flutter for OpenHarmony智能天气APP实战DAY5:OpenHarmony智能天气APP开发之新增未来7天天气预报展示
在前两篇教程中,我们已为Flutter for OpenHarmony天气预报App依次实现了「城市下拉选择」「自动定位」功能,实现了手动切换城市、自动获取当前城市并查看实时天气的核心需求。本次将单独新增「未来7天天气预报展示」功能,采用横向滑动卡片布局,清晰展示每日日期、天气图标、最高温/最低温,全程保持鸿蒙环境零报错、零新增依赖,完美适配OpenHarmony 5.1.0(18)版本,新手可直
Flutter for OpenHarmony智能天气APP实战DAY5:OpenHarmony智能天气APP开发之新增未来7天天气预报展示
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
前言
在前两篇教程中,我们已为Flutter for OpenHarmony天气预报App依次实现了「城市下拉选择」「自动定位」功能,实现了手动切换城市、自动获取当前城市并查看实时天气的核心需求。本次将单独新增「未来7天天气预报展示」功能,采用横向滑动卡片布局,清晰展示每日日期、天气图标、最高温/最低温,全程保持鸿蒙环境零报错、零新增依赖,完美适配OpenHarmony 5.1.0(18)版本,新手可直接复制代码复用,不影响原有所有功能。
本文核心:基于原有“自动定位+城市切换”功能,扩展未来7天天气预报逻辑,重点解决3个关键问题——天气接口扩展(获取7天预报数据)、UI布局适配(横向滑动卡片)、天气图标统一(与实时天气图标风格一致),同时保证鸿蒙环境兼容,不添加任何第三方插件,纯Flutter原生组件实现。
一、功能需求与UI设计
功能需求
- 在原有实时天气展示下方,新增“未来7天天气预报”模块,标题清晰可见;
- 每个预报卡片展示3个核心信息:日期(MM/dd格式)、天气图标(与实时天气图标风格统一)、最高温度/最低温度;
- 采用横向滑动布局,支持左右滑动查看7天预报,适配鸿蒙模拟器屏幕尺寸,无溢出问题;
- 数据联动:切换城市、重新定位后,7天预报数据同步刷新,与实时天气数据保持一致;
- 容错处理:当预报数据请求失败或无数据时,显示“暂无预报数据”提示,不崩溃、不报错;
- 保持原有所有功能(自动定位、城市切换、实时天气)不变,UI风格统一、简洁美观。
二、实现步骤
本次功能实现无需修改任何配置文件(pubspec.yaml、build-profile.json5、module.json5等),仅需修改main.dart文件,基于原有“自动定位+城市切换”功能扩展,核心分为5步,全程不影响原有功能,新手可按步骤修改,也可直接复制完整代码替换。
2.1 第一步:新增7天预报相关状态变量
在WeatherPage类中,新增用于存储7天预报数据的变量,用于UI渲染和数据判断,与原有状态变量统一管理
// 新增:存储未来7天预报数据(数组形式)
List<dynamic>? dailyForecast;
// 原有状态变量(保持不变)
Map<String, dynamic>? weatherData;
bool isLoading = true;
String errorMsg = '';
String currentCityName = "定位中...";
final List<Map<String, dynamic>> cities = [/* 原有城市列表 */];
late Map<String, dynamic> selectedCity;
2.2 第二步:修改天气接口,新增7天预报参数
修改原有apiUrl getter方法,在接口中新增daily参数(指定获取的预报字段)和forecast_days=8参数(获取8天数据),确保接口返回7天预报数据。
// 修改后:新增7天预报相关参数
String get apiUrl {
return 'https://api.open-meteo.com/v1/forecast?latitude=${selectedCity['lat']}&longitude=${selectedCity['lon']}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=Asia/Shanghai&forecast_days=8';
}
2.3 第三步:修改天气请求方法,解析7天预报数据
修改原有fetchWeatherData()方法,在解析实时天气数据的同时,解析daily字段,将7天预报数据封装为数组,存储到dailyForecast变量中,包含容错处理(无数据时设为null)。
Future<void> fetchWeatherData() async {
try {
final response = await get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
weatherData = data['current']; // 原有:实时天气数据
// 新增:解析7天预报数据(过滤当前日,取后面7天)
dailyForecast = data['daily']?['time'] != null
? List.generate(data['daily']['time'].length, (index) {
return {
"date": data['daily']['time'][index], // 日期
"max": data['daily']['temperature_2m_max'][index], // 最高温
"min": data['daily']['temperature_2m_min'][index], // 最低温
"code": data['daily']['weathercode'][index], // 天气代码(用于显示图标)
};
})
: null;
isLoading = false;
});
} else {
setState(() {
errorMsg = "请求失败";
isLoading = false;
});
}
} catch (e) {
setState(() {
errorMsg = "网络异常";
isLoading = false;
});
}
}
2.4 第四步:复用/优化天气图标方法,统一风格
原有天气图标方法(getWeatherIcon)仅用于实时天气,本次优化方法名称为weatherIcon,增加尺寸参数,使其可复用用于7天预报卡片(小尺寸图标)和实时天气(大尺寸图标),保持图标风格统一。
// 优化后:可复用的天气图标方法(支持自定义尺寸)
Widget weatherIcon(int code, double size) {
IconData icon;
Color color;
if (code == 0) {
icon = Icons.wb_sunny;
color = Colors.orange;
} else if (code >= 1 && code <= 3) {
icon = Icons.cloud;
color = Colors.grey;
} else if (code >= 45 && code <= 48) {
icon = Icons.foggy;
color = Colors.blueGrey;
} else if (code >= 51 && code <= 67) {
icon = Icons.water_drop;
color = Colors.blue;
} else if (code >= 71 && code <= 77) {
icon = Icons.snowing;
color = Colors.lightBlue;
} else if (code >= 80 && code <= 82) {
icon = Icons.thunderstorm;
color = Colors.deepPurple;
} else {
icon = Icons.cloud;
color = Colors.grey;
}
return Icon(icon, color: color, size: size);
}
2.5 第五步:新增7天预报UI模块,实现横向滑动卡片
在原有UI布局中,新增“未来7天天气预报”模块,放在“湿度/体感温度”卡片下方、“重新定位”按钮上方,采用横向ListView实现滑动卡片布局,渲染7天预报数据,包含容错处理(无数据时显示提示)。
// 新增:未来7天天气预报模块(放在湿度/体感温度卡片下方)
const SizedBox(height: 25),
const Text(
"📅 未来7天天气预报",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
// 7天预报横向滑动卡片
dailyForecast == null
? const Text("暂无预报数据")
: SizedBox(
height: 160, // 固定卡片高度,避免滑动时变形
child: ListView.builder(
scrollDirection: Axis.horizontal, // 横向滑动
itemCount: dailyForecast!.length - 1, // 过滤当前日,取后面7天
itemBuilder: (context, index) {
final day = dailyForecast![index + 1]; // 从第2天开始(跳过当前日)
return Container(
width: 110, // 每个卡片固定宽度
margin: const EdgeInsets.only(right: 8), // 卡片之间的间距
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12), // 圆角设计,与原有卡片统一
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 日期(MM/dd格式)
Text(
DateFormat('MM/dd').format(DateTime.parse(day['date'])),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
// 天气图标(小尺寸)
weatherIcon(day['code'], 28),
const SizedBox(height: 6),
// 最高温/最低温
Text("${day['max']}° / ${day['min']}°"),
],
),
);
},
),
),
三、完整修改后的main.dart代码(可直接复制)
以下是新增未来7天天气预报功能后的完整代码,整合了之前的自动定位、城市切换功能,无需修改任何配置文件,直接替换原有main.dart即可,确保鸿蒙模拟器零报错运行,所有功能正常联动。
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'dart:convert';
import 'package:intl/intl.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '鸿蒙天气预报',
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
),
home: const WeatherPage(),
debugShowCheckedModeBanner: false,
);
}
}
class WeatherPage extends StatefulWidget {
const WeatherPage({super.key});
@override
State<WeatherPage> createState() => _WeatherPageState();
}
class _WeatherPageState extends State<WeatherPage> {
Map<String, dynamic>? weatherData;
List<dynamic>? dailyForecast;
bool isLoading = true;
String errorMsg = '';
String currentCityName = "定位中...";
final List<Map<String, dynamic>> cities = [
{"name": "北京", "lat": 39.9042, "lon": 116.4074},
{"name": "上海", "lat": 31.2304, "lon": 121.4737},
{"name": "广州", "lat": 23.1291, "lon": 113.2644},
{"name": "深圳", "lat": 22.5431, "lon": 114.0579},
{"name": "杭州", "lat": 30.2741, "lon": 120.1551},
{"name": "成都", "lat": 30.5723, "lon": 104.0665},
{"name": "重庆", "lat": 29.5630, "lon": 106.5516},
];
late Map<String, dynamic> selectedCity;
@override
void initState() {
super.initState();
selectedCity = cities[0];
getLocationAndWeather();
}
Future<void> getLocationAndWeather() async {
try {
final ipResponse = await get(Uri.parse('https://api.ipify.org?format=json'));
final ip = json.decode(ipResponse.body)['ip'];
final locResponse = await get(Uri.parse('http://ip-api.com/json/$ip?lang=zh-CN'));
final data = json.decode(locResponse.body);
double lat = data['lat'] ?? 39.9042;
double lon = data['lon'] ?? 116.4074;
String city = data['city'] ?? "北京";
setState(() {
currentCityName = city;
selectedCity = {"name": city, "lat": lat, "lon": lon};
});
fetchWeatherData();
} catch (e) {
setState(() {
currentCityName = "北京";
selectedCity = cities[0];
});
fetchWeatherData();
}
}
String get apiUrl {
return 'https://api.open-meteo.com/v1/forecast?latitude=${selectedCity['lat']}&longitude=${selectedCity['lon']}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=Asia/Shanghai&forecast_days=8';
}
void changeCity(Map<String, dynamic> city) {
setState(() {
selectedCity = city;
currentCityName = city['name'];
isLoading = true;
errorMsg = '';
});
fetchWeatherData();
}
Future<void> fetchWeatherData() async {
try {
final response = await get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
weatherData = data['current'];
dailyForecast = data['daily']?['time'] != null
? List.generate(data['daily']['time'].length, (index) {
return {
"date": data['daily']['time'][index],
"max": data['daily']['temperature_2m_max'][index],
"min": data['daily']['temperature_2m_min'][index],
"code": data['daily']['weathercode'][index],
};
})
: null;
isLoading = false;
});
} else {
setState(() {
errorMsg = "请求失败";
isLoading = false;
});
}
} catch (e) {
setState(() {
errorMsg = "网络异常";
isLoading = false;
});
}
}
Widget weatherIcon(int code, double size) {
IconData icon;
Color color;
if (code == 0) {
icon = Icons.wb_sunny;
color = Colors.orange;
} else if (code >= 1 && code <= 3) {
icon = Icons.cloud;
color = Colors.grey;
} else if (code >= 45 && code <= 48) {
icon = Icons.foggy;
color = Colors.blueGrey;
} else if (code >= 51 && code <= 67) {
icon = Icons.water_drop;
color = Colors.blue;
} else if (code >= 71 && code <= 77) {
icon = Icons.snowing;
color = Colors.lightBlue;
} else if (code >= 80 && code <= 82) {
icon = Icons.thunderstorm;
color = Colors.deepPurple;
} else {
icon = Icons.cloud;
color = Colors.grey;
}
return Icon(icon, color: color, size: size);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.lightBlue[50],
body: isLoading
? const Center(child: CircularProgressIndicator())
: errorMsg.isNotEmpty
? Center(child: Text(errorMsg))
: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
const SizedBox(height: 40),
const Text(
"鸿蒙天气预报",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Text(
"当前城市:$currentCityName",
style: const TextStyle(fontSize: 16, color: Colors.blue, fontWeight: FontWeight.w500),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Map<String, dynamic>>(
value: selectedCity,
items: cities.map((c) {
return DropdownMenuItem(
value: c,
child: Text(c['name']),
);
}).toList(),
onChanged: (v) {
if (v != null) changeCity(v);
},
),
),
),
const SizedBox(height: 20),
Text(
DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()),
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 20),
weatherIcon(weatherData!['weather_code'], 100),
const SizedBox(height: 10),
Text(
'${weatherData!['temperature_2m']}°C',
style: const TextStyle(fontSize: 42, fontWeight: FontWeight.bold),
),
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
const Text("湿度"),
Text("${weatherData!['relative_humidity_2m']}%"),
],
),
Column(
children: [
const Text("体感"),
Text("${weatherData!['apparent_temperature']}°C"),
],
),
],
),
),
const SizedBox(height: 25),
const Text(
"📅 未来7天天气预报",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
dailyForecast == null
? const Text("暂无预报数据")
: SizedBox(
height: 160,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: dailyForecast!.length - 1,
itemBuilder: (context, index) {
final day = dailyForecast![index + 1];
return Container(
width: 110,
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
DateFormat('MM/dd').format(DateTime.parse(day['date'])),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
weatherIcon(day['code'], 28),
const SizedBox(height: 6),
Text("${day['max']}° / ${day['min']}°"),
],
),
);
},
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
setState(() {
isLoading = true;
currentCityName = "定位中...";
});
getLocationAndWeather();
},
child: const Text("重新定位"),
),
const SizedBox(height: 20),
],
),
),
);
}
}
四、鸿蒙适配与功能测试
5.1 测试步骤(无需额外配置)
- 替换main.dart文件后,在Android Studio终端执行 flutter pub get(无需新增任何依赖,仅确认原有依赖正常);
- 打开DevEco Studio,单独打开ohos目录,启动鸿蒙模拟器(OpenHarmony 5.1.0(18));
- 点击运行按钮,等待应用编译安装,启动后自动定位当前城市,同步加载实时天气和7天预报数据;
- 测试场景:左右滑动查看7天预报、切换城市(验证预报数据同步刷新)、重新定位(验证预报数据同步更新)、网络异常(验证无数据提示)。

五、常见问题解决(新手必看)
问题1:7天预报无数据,显示“暂无预报数据”?
原因:接口请求参数错误,或网络异常导致daily字段未返回数据;或forecast_days参数设置错误。
解决方案:检查apiUrl中的参数,确保包含daily=temperature_2m_max,temperature_2m_min,weathercode和forecast_days=8;检查模拟器网络连接,重启模拟器后重新运行。
问题2:7天预报卡片显示异常、溢出屏幕?
原因:卡片宽度未固定,或ListView未设置固定高度,导致自适应异常。
解决方案:确保Container设置固定宽度(如110),SizedBox设置固定高度(如160),避免自适应导致的溢出问题。
问题3:天气图标显示异常,与实时天气风格不一致?
原因:未复用统一的weatherIcon方法,或天气代码判断逻辑不一致。
解决方案:确保实时天气和7天预报均使用weatherIcon方法,天气代码判断逻辑保持一致,不单独修改。
问题4:切换城市后,7天预报数据未刷新?
原因:changeCity()方法中未调用fetchWeatherData(),或fetchWeatherData()方法未重新解析dailyForecast数据。
解决方案:检查changeCity()方法,确保调用fetchWeatherData();检查fetchWeatherData()方法,确保每次请求都重新解析dailyForecast。
总结
本次新增的未来7天天气预报功能,基于原有Flutter for OpenHarmony天气预报App,仅修改main.dart文件,无需调整任何配置,实现了横向滑动卡片展示7天预报数据的核心需求,同时保持了与原有功能(自动定位、城市切换)的完美联动。
核心亮点在于“零新增依赖、接口扩展实现、UI风格统一”,通过扩展原有天气接口参数,避免了新增接口带来的兼容性问题;采用Flutter原生横向ListView组件,确保鸿蒙环境下滑动流畅、UI适配;完善的容错处理,确保App在各种场景下(无数据、网络异常)均能正常运行,不崩溃、不报错。
对于Flutter for OpenHarmony开发新手来说,本次开发再次验证了“复用现有逻辑、优先使用原生组件”的适配原则,无需复杂配置,仅通过简单的接口扩展和UI渲染,就能实现实用功能。本文代码已在OpenHarmony 5.1.0模拟器上验证通过,可直接复制到项目中使用,后续可根据需求继续扩展,提升App体验!
更多推荐
所有评论(0)