## 一、前言:为什么选择 Flutter 开发天气 App?

移动设备上的天气应用是用户每日必用的功能之一。它结构清晰(数据 + UI)、交互简单,非常适合作为 Flutter 的入门实战项目。

在本篇文章中,我们将使用 **Flutter + Dart + OpenWeatherMap API** 构建一个跨平台的天气应用,支持 Android、iOS 和 Web。

---

## 二、项目预览图

| 页面 | 截图 |
|------|------|
| 主界面 | ![天气主界面](https://via.placeholder.com/300x600?text=Weather+App+UI) |
| 加载状态 | ![加载中](https://via.placeholder.com/300x600?text=Loading...) |
| 错误提示 | ![网络错误](https://via.placeholder.com/300x600?text=Network+Error) |

> 🔗 实际开发建议替换为真实截图。此处为占位图示意。

---

## 三、环境准备与依赖配置

### 1. 创建项目

```bash
flutter create weather_app
cd weather_app
```

### 2. 添加依赖(`pubspec.yaml`)

```yaml
dependencies:
  flutter:
    sdk: flutter
  http: ^0.15.0     # 网络请求
  intl: ^0.19.0     # 格式化时间、数字等
  get: ^4.6.6       # 路由与状态管理(可选)
```

运行命令安装:

```bash
flutter pub get
```

---

## 四、获取免费天气 API 密钥

我们使用 [OpenWeatherMap](https://openweathermap.org/api) 提供的免费 API。

1. 注册账号:https://home.openweathermap.org/users/sign_up
2. 获取 API Key(形如 `abc123xyz`)
3. 使用城市天气接口:
   ```
   https://api.openweathermap.org/data/2.5/weather?q=Beijing&appid=YOUR_API_KEY&units=metric&lang=zh_cn
   ```

参数说明:

- `q=Beijing`:城市名
- `units=metric`:摄氏度
- `lang=zh_cn`:中文语言

---

## 五、定义数据模型(Model)

创建文件 `lib/model/weather.dart`

```dart
class Weather {
  final String city;
  final double temperature;
  final String description;
  final String iconCode;

  Weather({
    required this.city,
    required this.temperature,
    required this.description,
    required this.iconCode,
  });

  // 工厂构造函数:从 JSON 创建对象
  factory Weather.fromJson(Map<String, dynamic> json) {
    return Weather(
      city: json['name'],
      temperature: json['main']['temp'].toDouble(),
      description: json['weather'][0]['description'],
      iconCode: json['weather'][0]['icon'],
    );
  }
}
```

---

## 六、网络请求服务(Service)

创建 `lib/service/weather_service.dart`

```dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../model/weather.dart';

class WeatherService {
  final String apiKey = 'YOUR_API_KEY_HERE'; // 替换为你自己的 key

  Future<Weather?> getWeather(String cityName) async {
    try {
      final response = await http.get(Uri.parse(
        'https://api.openweathermap.org/data/2.5/weather?q=$cityName&appid=$apiKey&units=metric&lang=zh_cn',
      ));

      if (response.statusCode == 200) {
        final data = json.decode(response.body);
        return Weather.fromJson(data);
      } else {
        print('Error: ${response.statusCode}');
        return null;
      }
    } catch (e) {
      print('Network Error: $e');
      return null;
    }
  }
}
```

---

## 七、构建 UI 界面

修改 `lib/main.dart`

```dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../service/weather_service.dart';
import '../model/weather.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter 天气',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue),
        useMaterial3: true,
      ),
      home: const WeatherPage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class WeatherPage extends StatefulWidget {
  const WeatherPage({super.key});

  @override
  State<WeatherPage> createState() => _WeatherPageState();
}

class _WeatherPageState extends State<WeatherPage> {
  final WeatherService service = WeatherService();
  Weather? _weather;
  bool _loading = false;
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Future<void> _fetchWeather() async {
    final city = _controller.text.trim();
    if (city.isEmpty) return;

    setState(() {
      _loading = true;
      _weather = null;
    });

    final result = await service.getWeather(city);
    setState(() {
      _loading = false;
      _weather = result;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("🌤️ 实时天气"),
        centerTitle: true,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 搜索栏
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: "输入城市名称,如:北京",
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _fetchWeather(),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton(
                  icon: const Icon(Icons.search),
                  onPressed: _fetchWeather,
                )
              ],
            ),

            const SizedBox(height: 20),

            // 加载动画
            if (_loading)
              const CircularProgressIndicator()
            else if (_weather != null)
              _buildWeatherCard(_weather!)
            else if (!_loading && _weather == null)
              const Text("暂无数据,请检查城市名或网络", style: TextStyle(color: Colors.grey))
          ],
        ),
      ),
    );
  }

  Widget _buildWeatherCard(Weather weather) {
    final temp = weather.temperature.toInt();
    final iconUrl = 'https://openweathermap.org/img/wn/${weather.iconCode}@2x.png';
    final now = DateFormat.Hm().format(DateTime.now());

    return Card(
      elevation: 4,
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Text(weather.city, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            Text("$now 更新", style: const TextStyle(color: Colors.grey)),
            const SizedBox(height: 16),
            Image.network(iconUrl),
            Text("$temp°C", style: const TextStyle(fontSize: 40, fontWeight: FontWeight.w300)),
            Text(weather.description.toUpperCase(), style: const TextStyle(fontSize: 16)),
          ],
        ),
      ),
    );
  }
}
```

---

## 八、代码结构解析

```
/lib
 ├── main.dart           # 入口 & 页面逻辑
 ├── model/
 │    └── weather.dart   # 数据模型
 └── service/
      └── weather_service.dart  # 网络请求
```

这种分层结构让项目更易于维护和扩展。

---

## 九、运行项目

```bash
# 运行在安卓模拟器
flutter run

# 运行在 iOS 模拟器
flutter run -d iPhone

# 运行在 Chrome 浏览器(Web 版)
flutter run -d chrome
```

✅ 成功运行后,输入“上海”、“纽约”,即可看到实时天气!

---

## 十、优化建议(进阶方向)

| 功能 | 实现方式 |
|------|---------|
| 自动定位 | 使用 `geolocator` 插件获取 GPS |
| 多城市收藏 | `shared_preferences` 存储历史记录 |
| 更多天气信息 | 显示湿度、风速、气压 |
| 状态管理 | 使用 `Provider` 或 `Riverpod` 管理全局状态 |
| 主题切换 | 支持深色/浅色模式 |

---

## 十一、打包发布

### Android

```bash
flutter build apk --release
```

生成路径:`build/app/outputs/flutter-apk/app-release.apk`

### Web

```bash
flutter build web
```

将 `build/web` 文件夹部署到 GitHub Pages、Vercel 或 Netlify。

---

## 十二、总结

通过这个天气 App,你已经掌握了 Flutter 的核心技能:

- ✅ 组件布局(Scaffold、Column、Row、Card)
- ✅ 网络请求(http + JSON 解析)
- ✅ 异步编程(Future)
- ✅ 表单输入与事件处理
- ✅ 第三方 API 集成

> 🎉 **恭喜你完成了第一个完整的 Flutter 应用!**

---

## 十三、常见问题 FAQ

❓ **Q:API 请求失败怎么办?**  
👉 检查 API Key 是否正确,是否替换了 `YOUR_API_KEY_HERE`。

❓ **Q:图标不显示?**  
👉 确保 `AndroidManifest.xml` 中添加了网络权限:
```xml
<uses-permission android:name="android.permission.INTERNET"/>
```

❓ **Q:如何支持 HTTPS 图片?**  
👉 所有现代 Flutter 项目默认支持 HTTPS,无需额外配置。

---

## 十四、结语

Flutter 正在改变跨平台开发的格局。它不仅高效、美观,还能让你用一套代码覆盖多个平台。

下一步你可以尝试:

- 增加未来 7 天预报(调用 `forecast` 接口)
- 使用地图选择城市
- 添加动画过渡效果

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐