###Flutter 实战教程:构建一个天气应用
本教程将带领大家使用 Flutter 构建一个功能完整的天气应用,涵盖 UI 设计、API 调用、状态管理等核心知识点。我们将使用 OpenWeatherMap API 获取实时天气数据,并通过 Provider 实现高效的状态管理。
项目初始化
- 创建新的 Flutter 项目并进入项目目录:
bash
flutter create weather_app
cd weather_app
- 在
pubspec.yaml中添加以下依赖:
yaml
dependencies:
flutter:
sdk: flutter
http: ^0.13.5 # 用于网络请求
provider: ^6.0.5 # 状态管理
flutter_spinkit: ^5.1.0 # 加载动画
intl: ^0.18.1 # 日期格式化
- 运行命令安装依赖:
bash
flutter pub get
# 网络请求封装详解
1. API 账号注册与密钥获取
在使用 OpenWeatherMap API 之前,需要先完成以下步骤:
- 访问 OpenWeatherMap 官网 并注册免费账号
- 登录后进入 "API Keys" 页面
- 生成一个新的 API Key(免费版每天允许 60 次调用)
- 重要提示:请妥善保管您的 API Key,不要直接提交到公共代码仓库
2. 天气服务类实现
在 lib/services/weather_service.dart 文件中,我们创建了一个完整的天气服务类:
dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class WeatherService {
// 替换为你的实际API Key
static const String _apiKey = "YOUR_API_KEY_HERE";
// OpenWeatherMap API 基础URL
static const String _baseUrl = "https://api.openweathermap.org/data/2.5/weather";
// 单位设置为公制(metric)获取摄氏度温度
static const String _units = "metric";
/// 获取指定城市的天气数据
/// [city] 城市名称 (如 "London" 或 "Beijing")
/// 返回包含天气数据的Map
/// 可能抛出异常: "City not found", "API request failed" 或网络错误
Future<Map<String, dynamic>> fetchWeather(String city) async {
try {
// 构造完整的API请求URL
final uri = Uri.parse("$_baseUrl?q=$city&appid=$_apiKey&units=$_units");
// 发送HTTP GET请求
final response = await http.get(uri);
// 处理响应状态码
if (response.statusCode == 200) {
// 成功响应,解析JSON数据
return json.decode(response.body);
} else if (response.statusCode == 404) {
throw Exception("City not found");
} else {
throw Exception("API request failed with status ${response.statusCode}");
}
} catch (e) {
// 捕获并处理网络请求过程中的异常
throw Exception("Network error: ${e.toString()}");
}
}
/// 根据天气图标代码生成完整的图标URL
/// [iconCode] 来自API响应的天气图标代码 (如 "01d" 表示晴天)
/// 返回完整的图标URL
String getWeatherIcon(String iconCode) {
// 使用@2x获取高清版本图标
return "https://openweathermap.org/img/wn/$iconCode@2x.png";
}
}
3. 使用示例
dart
// 创建天气服务实例
final weatherService = WeatherService();
try {
// 获取天气数据
final weatherData = await weatherService.fetchWeather("London");
// 解析温度数据
final temp = weatherData['main']['temp'];
final description = weatherData['weather'][0]['description'];
// 获取天气图标
final iconUrl = weatherService.getWeatherIcon(weatherData['weather'][0]['icon']);
print('当前温度: $temp°C');
print('天气状况: $description');
print('图标URL: $iconUrl');
} catch (e) {
print('获取天气数据失败: $e');
}
4. 错误处理建议
-
城市不存在错误 (404): 提示用户检查城市名称拼写
-
API请求失败: 检查API Key是否有效或是否超过调用限制
-
网络错误: 检查设备网络连接状态
-
建议在UI层显示友好的错误提示信息
}状态管理
创建
lib/providers/weather_provider.dart文件:dartimport 'package:flutter/material.dart'; import '../services/weather_service.dart'; class WeatherProvider extends ChangeNotifier { Map<String, dynamic>? _weatherData; String _error = ""; bool _isLoading = false; Map<String, dynamic>? get weatherData => _weatherData; String get error => _error; bool get isLoading => _isLoading; Future<void> fetchWeather(String city) async { _isLoading = true; notifyListeners(); try { _weatherData = await WeatherService().fetchWeather(city); _error = ""; } catch (e) { _error = e.toString(); _weatherData = null; } _isLoading = false; notifyListeners(); } }
UI 界面设计
- 创建
lib/screens/weather_screen.dart文件:
dart
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../providers/weather_provider.dart';
class WeatherScreen extends StatefulWidget {
@override
_WeatherScreenState createState() => _WeatherScreenState();
}
class _WeatherScreenState extends State<WeatherScreen> {
final TextEditingController _cityController = TextEditingController();
final FocusNode _cityFocus = FocusNode();
@override
void dispose() {
_cityController.dispose();
_cityFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final weatherProvider = Provider.of<WeatherProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text("Weather App"),
centerTitle: true,
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 搜索框
TextField(
controller: _cityController,
focusNode: _cityFocus,
decoration: InputDecoration(
labelText: "Enter City",
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(Icons.search),
onPressed: () {
if (_cityController.text.isNotEmpty) {
weatherProvider.fetchWeather(_cityController.text);
_cityFocus.unfocus();
}
},
),
),
onSubmitted: (value) {
if (value.isNotEmpty) {
weatherProvider.fetchWeather(value);
}
},
),
SizedBox(height: 20),
// 加载状态
if (weatherProvider.isLoading)
Center(
child: SpinKitWave(
color: Colors.blue,
size: 50.0,
),
),
// 错误提示
if (weatherProvider.error.isNotEmpty && !weatherProvider.isLoading)
Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.red[100],
borderRadius: BorderRadius.circular(5),
),
child: Text(
weatherProvider.error,
style: TextStyle(color: Colors.red[800]),
),
),
// 天气信息展示
if (weatherProvider.weatherData != null && !weatherProvider.isLoading)
Expanded(
child: Card(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 城市名称和日期
Text(
"${weatherProvider.weatherData!['name']}",
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
Text(
DateFormat('EEEE, MMMM d').format(DateTime.now()),
style: TextStyle(fontSize: 16),
),
SizedBox(height: 30),
// 天气图标和温度
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network(
WeatherService().getWeatherIcon(
weatherProvider.weatherData!['weather'][0]['icon']
),
width: 100,
height: 100,
),
SizedBox(width: 20),
Text(
"${weatherProvider.weatherData!['main']['temp']?.toStringAsFixed(1)}°C",
style: TextStyle(
fontSize: 60,
fontWeight: FontWeight.w300,
),
),
],
),
SizedBox(height: 20),
// 天气描述
Text(
"${weatherProvider.weatherData!['weather'][0]['description']}"
.toUpperCase(),
style: TextStyle(
fontSize: 20,
letterSpacing: 1.2,
),
),
SizedBox(height: 30),
// 详细信息
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildDetailItem(
Icons.air,
"${weatherProvider.weatherData!['wind']['speed']} km/h",
"Wind",
),
_buildDetailItem(
Icons.water_drop,
"${weatherProvider.weatherData!['main']['humidity']}%",
"Humidity",
),
_buildDetailItem(
Icons.compress,
"${weatherProvider.weatherData!['main']['pressure']} hPa",
"Pressure",
),
],
),
],
),
),
),
),
],
),
),
);
}
Widget _buildDetailItem(IconData icon, String value, String label) {
return Column(
children: [
Icon(icon, size: 30, color: Colors.blue),
SizedBox(height: 5),
Text(value, style: TextStyle(fontSize: 16)),
SizedBox(height: 5),
Text(label, style: TextStyle(fontSize: 14, color: Colors.grey)),
],
);
}
}
主程序入口
修改 lib/main.dart 文件:
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weather_app/providers/weather_provider.dart';
import 'package:weather_app/screens/weather_screen.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => WeatherProvider()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Weather App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: WeatherScreen(),
);
}
}
# 天气应用功能扩展建议
核心功能增强
-
位置服务集成
- 使用
geolocator包获取设备当前位置,支持Android和iOS平台 - 实现自动定位功能,优先显示用户所在城市天气,需处理定位权限请求流程
- 提供手动切换城市选项(输入城市名称如"London"、"北京"),支持城市名称自动补全功能
- 使用
-
天气预报扩展
- 实现多天天气预报功能(3天/7天预报),使用卡片式布局展示每日天气概况
- 增加小时级天气预报显示,支持左右滑动查看24小时天气变化
- 添加天气趋势图表(温度变化曲线),使用
charts_flutter包实现可视化数据展示
-
天气数据完善
- 显示更多气象信息:
- 湿度(如:65%),附带舒适度提示
- 风速及风向(如:东南风3级),使用风向图标直观展示
- 气压(如:1012hPa),标注高低气压状态
- 紫外线指数,附带防护建议
- 能见度,适用于交通出行参考
- 使用
flutter_icons包添加动态天气图标匹配,如晴天显示太阳图标,雨天显示雨伞图标
- 显示更多气象信息:
用户体验优化
-
主题切换功能
- 提供日间/夜间模式切换,支持系统主题自动跟随
- 根据实际天气自动调整主题配色,如雨天使用蓝色系,晴天使用黄色系
- 自定义主题颜色选项,允许用户保存个人偏好设置
-
数据缓存机制
- 实现天气数据的本地缓存(使用shared_preferences),减少网络请求
- 设置缓存过期时间(如30分钟),确保数据时效性
- 离线时显示最近缓存数据,并提示"离线模式"状态
-
交互增强
- 添加天气预警通知功能,支持暴雨、高温等极端天气提醒
- 实现天气数据刷新动画,如下拉刷新时的加载效果
- 增加天气详情展开/收起交互,优化信息展示空间
开发实践要点
通过本教程可以掌握的Flutter核心开发技能:
- 网络请求处理(REST API调用):包括错误处理、超时设置和响应解析
- 复杂状态管理(Provider/Bloc):实现跨组件数据共享和状态同步
- 响应式UI构建:适配不同屏幕尺寸和设备方向
- 平台特性集成(GPS、通知等):处理原生平台权限和功能调用
- 数据持久化方案:本地存储和缓存策略实现
- 性能优化实践:减少Widget重建、图片缓存等优化技巧