Flutter 实战教程:构建一个天气应用

###Flutter 实战教程:构建一个天气应用

本教程将带领大家使用 Flutter 构建一个功能完整的天气应用,涵盖 UI 设计、API 调用、状态管理等核心知识点。我们将使用 OpenWeatherMap API 获取实时天气数据,并通过 Provider 实现高效的状态管理。

项目初始化
  1. 创建新的 Flutter 项目并进入项目目录:
bash 复制代码
flutter create weather_app
cd weather_app
  1. pubspec.yaml 中添加以下依赖:
yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.5  # 用于网络请求
  provider: ^6.0.5  # 状态管理
  flutter_spinkit: ^5.1.0  # 加载动画
  intl: ^0.18.1  # 日期格式化
  1. 运行命令安装依赖:
bash 复制代码
flutter pub get
# 网络请求封装详解

1. API 账号注册与密钥获取

在使用 OpenWeatherMap API 之前,需要先完成以下步骤:

  1. 访问 OpenWeatherMap 官网 并注册免费账号
  2. 登录后进入 "API Keys" 页面
  3. 生成一个新的 API Key(免费版每天允许 60 次调用)
  4. 重要提示:请妥善保管您的 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. 错误处理建议

  1. 城市不存在错误 (404): 提示用户检查城市名称拼写

  2. API请求失败: 检查API Key是否有效或是否超过调用限制

  3. 网络错误: 检查设备网络连接状态

  4. 建议在UI层显示友好的错误提示信息
    }

    状态管理

    创建 lib/providers/weather_provider.dart 文件:

    dart 复制代码
    import '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 界面设计
  1. 创建 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(),
    );
  }
}
# 天气应用功能扩展建议

核心功能增强

  1. 位置服务集成

    • 使用 geolocator 包获取设备当前位置,支持Android和iOS平台
    • 实现自动定位功能,优先显示用户所在城市天气,需处理定位权限请求流程
    • 提供手动切换城市选项(输入城市名称如"London"、"北京"),支持城市名称自动补全功能
  2. 天气预报扩展

    • 实现多天天气预报功能(3天/7天预报),使用卡片式布局展示每日天气概况
    • 增加小时级天气预报显示,支持左右滑动查看24小时天气变化
    • 添加天气趋势图表(温度变化曲线),使用charts_flutter包实现可视化数据展示
  3. 天气数据完善

    • 显示更多气象信息:
      • 湿度(如:65%),附带舒适度提示
      • 风速及风向(如:东南风3级),使用风向图标直观展示
      • 气压(如:1012hPa),标注高低气压状态
      • 紫外线指数,附带防护建议
      • 能见度,适用于交通出行参考
    • 使用 flutter_icons 包添加动态天气图标匹配,如晴天显示太阳图标,雨天显示雨伞图标

用户体验优化

  1. 主题切换功能

    • 提供日间/夜间模式切换,支持系统主题自动跟随
    • 根据实际天气自动调整主题配色,如雨天使用蓝色系,晴天使用黄色系
    • 自定义主题颜色选项,允许用户保存个人偏好设置
  2. 数据缓存机制

    • 实现天气数据的本地缓存(使用shared_preferences),减少网络请求
    • 设置缓存过期时间(如30分钟),确保数据时效性
    • 离线时显示最近缓存数据,并提示"离线模式"状态
  3. 交互增强

    • 添加天气预警通知功能,支持暴雨、高温等极端天气提醒
    • 实现天气数据刷新动画,如下拉刷新时的加载效果
    • 增加天气详情展开/收起交互,优化信息展示空间

开发实践要点

通过本教程可以掌握的Flutter核心开发技能:

  • 网络请求处理(REST API调用):包括错误处理、超时设置和响应解析
  • 复杂状态管理(Provider/Bloc):实现跨组件数据共享和状态同步
  • 响应式UI构建:适配不同屏幕尺寸和设备方向
  • 平台特性集成(GPS、通知等):处理原生平台权限和功能调用
  • 数据持久化方案:本地存储和缓存策略实现
  • 性能优化实践:减少Widget重建、图片缓存等优化技巧
相关推荐
克喵的水银蛇1 小时前
Flutter 通用列表项封装实战:适配多场景的 ListItemWidget
前端·javascript·flutter
Non-existent9871 小时前
Flutter + FastAPI 30天速成计划自用并实践-第7天
flutter·oracle·fastapi
帅气马战的账号1 小时前
开源鸿蒙+Flutter:跨端开发的分布式协同与数据互通实践
flutter
longforus2 小时前
Flutter iOS 真机部署异常经验(Android Studio 提示无法运行,但 Xcode 可正常运行)
flutter·ios·android studio
晚霞的不甘2 小时前
实战深研:构建高可靠、低延迟的 Flutter + OpenHarmony 智慧教育互动平台(支持离线教学、多端协同与国产化适配)
前端·javascript·flutter
kirk_wang2 小时前
Flutter视频播放器在鸿蒙系统(HarmonyOS)上的适配实践
flutter·移动开发·跨平台·arkts·鸿蒙
子春一2 小时前
Flutter 与 Web3 融合开发实战:在去中心化应用(DApp)中集成钱包、智能合约与链上交互
flutter·web3·去中心化
晚霞的不甘2 小时前
实战精研:构建高安全、多模态的 Flutter + OpenHarmony 智慧医疗健康应用(符合 HIPAA 与医疗器械软件规范)
javascript·安全·flutter
小a彤2 小时前
Flutter 与原生开发深度对比及实践指南
flutter