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']}&current=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']}&current=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 测试步骤

  1. 替换main.dart文件后,在Android Studio终端执行flutter pub get(无需新增依赖,仅确认原有依赖正常);
  2. 打开DevEco Studio,单独打开ohos目录,启动鸿蒙模拟器(OpenHarmony 5.1.0(18));
  3. 点击运行按钮,等待应用编译安装,启动后即可看到城市选择下拉框;
  4. 点击下拉框切换城市,观察是否自动加载并显示对应城市的天气数据。
    在这里插入图片描述

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模拟器上验证通过,放心复用!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐