Flutter for OpenHarmony智能天气APP实战DAY3:OpenHarmony智能天气APP开发之增加城市选择功能,支持切换不同城市的天气
在上一篇中,我们已经实现了基础的天气展示功能,可正常在鸿蒙模拟器运行。本次将单独新增「城市选择功能」,支持下拉切换多个城市、自动刷新对应城市天气,全程保持鸿蒙零报错适配,不添加任何多余依赖,新手可直接复制复用代码。本文核心:基于原有鸿蒙适配版天气预报App,新增城市下拉选择、城市切换联动天气刷新,解决城市切换时的数据同步、加载状态管理问题,确保功能流畅且完全适配OpenHarmony 5.1.0(
Flutter for OpenHarmony智能天气APP实战DAY3:OpenHarmony智能天气APP开发之增加城市选择功能,支持切换不同城市的天气
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
前言
在上一篇中,我们已经实现了基础的天气展示功能,可正常在鸿蒙模拟器运行。本次将单独新增「城市选择功能」,支持下拉切换多个城市、自动刷新对应城市天气,全程保持鸿蒙零报错适配,不添加任何多余依赖,新手可直接复制复用代码。
本文核心:基于原有鸿蒙适配版天气预报App,新增城市下拉选择、城市切换联动天气刷新,解决城市切换时的数据同步、加载状态管理问题,确保功能流畅且完全适配OpenHarmony 5.1.0(18)版本。
一、功能需求与设计
1.1 功能需求
- 新增城市下拉选择框,默认选中北京
- 支持切换7个主流城市(可自由增删)
- 切换城市后,自动请求对应城市的天气数据,显示加载状态
- 保持原有功能(温度、湿度、体感温度、天气图标)不变
- 全程适配鸿蒙环境,无任何黄色报错、无本地化依赖冲突
1.2 界面设计
在原有界面基础上,在“鸿蒙天气预报”标题下方、时间显示上方,新增一个下拉选择框,样式简洁,与原有UI风格统一
二、核心实现步骤(重点)
本次功能实现无需修改任何配置文件(pubspec.yaml、build-profile.json5等),仅需修改main.dart文件,核心分为3步:定义城市列表、实现下拉选择组件、联动天气数据刷新。
2.1 第一步:定义城市列表(含经纬度)
天气接口(Open-Meteo)通过经纬度获取对应城市天气,因此我们需要为每个城市配置「名称+纬度(lat)+经度(lon)」,可自由增删城市。
核心代码片段(在WeatherPage类中添加):
// 城市列表(名称 + 经纬度,可自由增删)
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;
2.2 第二步:初始化选中城市
在initState方法中,初始化默认选中的城市(北京),并调用天气请求方法,确保页面加载时显示默认城市天气。
@override
void initState() {
super.initState();
selectedCity = cities[0]; // 默认选中第一个城市(北京)
fetchWeatherData(); // 初始化请求天气数据
}
2.3 第三步:修改天气接口,联动选中城市
将原来固定的北京经纬度,改为动态获取当前选中城市的经纬度,实现切换城市后请求对应天气。
// 动态获取当前选中城市的天气API(替代原来固定的北京接口)
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,precipitation,weather_code&timezone=Asia/Shanghai';
}
2.4 第四步:实现城市切换方法
定义切换城市的方法,切换后重置加载状态、清空错误信息,重新请求对应城市的天气数据。
// 切换城市方法
void changeCity(Map<String, dynamic> city) {
setState(() {
selectedCity = city; // 更新选中城市
isLoading = true; // 显示加载状态
errorMsg = ''; // 清空错误信息
});
fetchWeatherData(); // 重新请求选中城市的天气
}
2.5 第五步:添加下拉选择组件
在界面中添加下拉选择框,放在标题下方,样式与原有UI统一,隐藏默认下划线,提升美观度。
// 城市选择下拉框(放在标题和时间之间)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Map<String, dynamic>>(
value: selectedCity,
items: cities.map((city) {
return DropdownMenuItem(
value: city,
child: Text(
city['name'],
style: const TextStyle(fontSize: 18),
),
);
}).toList(),
onChanged: (val) {
if (val != null) changeCity(val); // 切换城市
},
),
),
)
三、完整修改后的main.dart代码(可直接复制)
以下是新增城市选择功能后的完整代码,替换原有main.dart即可,无需修改任何其他文件,确保鸿蒙模拟器零报错运行
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
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;
bool isLoading = true;
String errorMsg = '';
// 城市列表(名称 + 经纬度)
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];
fetchWeatherData();
}
// 获取天气API
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,precipitation,weather_code&timezone=Asia/Shanghai';
}
// 切换城市
void changeCity(Map<String, dynamic> city) {
setState(() {
selectedCity = city;
isLoading = true;
errorMsg = '';
});
fetchWeatherData();
}
// 请求天气数据
Future<void> fetchWeatherData() async {
try {
final response = await http.get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
setState(() {
weatherData = json.decode(response.body);
isLoading = false;
});
} else {
setState(() {
errorMsg = '数据请求失败';
isLoading = false;
});
}
} catch (e) {
setState(() {
errorMsg = '网络异常:$e';
isLoading = false;
});
}
}
// 天气图标
Widget getWeatherIcon(int code) {
IconData icon;
Color color;
String weatherText;
if (code == 0) {
icon = Icons.wb_sunny;
color = Colors.orangeAccent;
weatherText = '晴天';
} else if (code >= 1 && code <= 3) {
icon = Icons.cloud;
color = Colors.blueGrey;
weatherText = '多云';
} else if (code >= 45 && code <= 48) {
icon = Icons.foggy;
color = Colors.grey;
weatherText = '雾天';
} else if (code >= 51 && code <= 67) {
icon = Icons.water_drop;
color = Colors.blue;
weatherText = '雨天';
} else if (code >= 71 && code <= 77) {
icon = Icons.snowing;
color = Colors.lightBlue;
weatherText = '雪天';
} else if (code >= 80 && code <= 82) {
icon = Icons.thunderstorm;
color = Colors.deepPurple;
weatherText = '雷阵雨';
} else {
icon = Icons.cloud;
color = Colors.grey;
weatherText = '未知';
}
return Column(
children: [
Icon(icon, size: 120, color: color),
const SizedBox(height: 10),
Text(
weatherText,
style: TextStyle(fontSize: 22, color: color, fontWeight: FontWeight.w500),
),
],
);
}
@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: 15),
// 城市选择下拉框
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Map<String, dynamic>>(
value: selectedCity,
items: cities.map((city) {
return DropdownMenuItem(
value: city,
child: Text(
city['name'],
style: const TextStyle(fontSize: 18),
),
);
}).toList(),
onChanged: (val) {
if (val != null) changeCity(val);
},
),
),
),
const SizedBox(height: 20),
Text(
DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()),
style: const TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 30),
// 天气图标
getWeatherIcon(weatherData!['current']['weather_code']),
const SizedBox(height: 30),
// 温度
Text(
'${weatherData!['current']['temperature_2m']} °C',
style: const TextStyle(
fontSize: 50,
fontWeight: FontWeight.bold,
color: Colors.blueAccent,
),
),
const SizedBox(height: 15),
// 湿度 + 体感温度
Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
const Text('湿度', style: TextStyle(fontSize: 16)),
const SizedBox(height: 5),
Text(
'${weatherData!['current']['relative_humidity_2m']} %',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
],
),
Column(
children: [
const Text('体感温度', style: TextStyle(fontSize: 16)),
const SizedBox(height: 5),
Text(
'${weatherData!['current']['apparent_temperature']} °C',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
],
),
],
),
),
const SizedBox(height: 25),
// 刷新按钮
ElevatedButton(
onPressed: () {
setState(() {
isLoading = true;
errorMsg = '';
});
fetchWeatherData();
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: const Text('刷新天气', style: TextStyle(fontSize: 18)),
),
],
),
),
);
}
}
四、功能测试与鸿蒙适配说明
4.1 测试步骤
- 替换main.dart文件后,在Android Studio终端执行flutter pub get(无需新增依赖,仅确认原有依赖正常);
- 打开DevEco Studio,单独打开ohos目录,启动鸿蒙模拟器(OpenHarmony 5.1.0(18));
- 点击运行按钮,等待应用编译安装,启动后即可看到城市选择下拉框;
- 点击下拉框切换城市,观察是否自动加载并显示对应城市的天气数据。

4.2 鸿蒙适配关键点
- 未使用任何本地化相关组件,彻底规避No MaterialLocalizations found 报错;
- 未新增任何第三方依赖,仅使用Flutter原生组件(DropdownButton、Container等),确保鸿蒙兼容;
- 切换城市时添加加载状态,避免鸿蒙环境下数据刷新卡顿;
- 下拉框样式简洁,适配鸿蒙模拟器屏幕尺寸,无溢出问题。
五、常见问题解决
问题1:切换城市后,天气数据不刷新?
解决方案:检查changeCity方法是否调用了fetchWeatherData(),确保setState正确更新selectedCity。
问题2:下拉框显示异常、有下划线?
解决方案:确保使用了DropdownButtonHideUnderline组件,隐藏默认下划线,同时检查Container的边框和圆角配置。
问题3:鸿蒙模拟器中下拉框无法点击?
解决方案:检查是否有组件遮挡下拉框,或模拟器是否正常启动,可重启模拟器重新运行。
六、总结
本次新增的城市选择功能,基于原有鸿蒙适配版天气预报App,仅修改main.dart文件,无需调整任何配置,实现了下拉切换城市、自动刷新天气的核心需求,且全程保持鸿蒙零报错、无依赖冲突。
对于Flutter for OpenHarmony开发新手来说,重点注意:鸿蒙环境下尽量使用Flutter原生组件,避免添加不必要的依赖,遇到本地化相关报错可通过简化UI组件(如下拉框替代AppBar)规避。
后续可根据需求继续扩展功能,本文代码可直接复制到项目中使用,已在OpenHarmony 5.1.0模拟器上验证通过,放心复用!
更多推荐



所有评论(0)