1. 项目背景与核心需求
天气预报应用作为移动开发领域的经典练手项目,看似简单却涵盖了网络请求、数据解析、UI渲染等核心开发技能。这次我们要用Flutter框架结合retrofit库,对接和风天气API,打造一个鸿蒙系统兼容的天气应用城市卡片组件。
选择这个技术栈有几个关键考量:首先Flutter的跨平台特性让我们可以一套代码同时覆盖Android、iOS和鸿蒙系统;其次retrofit作为Dart语言的类型安全HTTP客户端,能极大简化API请求和响应处理的复杂度;最后和风天气API提供稳定可靠的天气数据服务,免费套餐完全够个人开发者使用。
2. 技术选型与准备工作
2.1 开发环境搭建
首先确保你的开发环境已经配置好Flutter SDK(建议3.0以上版本)和鸿蒙开发工具。在pubspec.yaml中添加以下依赖:
dependencies: flutter: sdk: flutter retrofit: ^3.3.1 dio: ^4.0.6 json_annotation: ^4.8.0 logger: ^1.1.0 dev_dependencies: build_runner: ^2.1.11 retrofit_generator: ^3.1.0 json_serializable: ^6.5.4运行flutter pub get安装依赖后,我们需要在和风天气官网注册开发者账号,获取API Key。建议选择免费版的"开发版"套餐,每天1000次调用足够开发测试使用。
2.2 项目结构设计
合理的项目结构能提高代码可维护性。建议采用以下目录结构:
lib/ ├── api/ # API相关文件 │ ├── weather_api.dart │ └── models/ # 数据模型 ├── widgets/ # 自定义组件 │ └── weather_card.dart ├── utils/ # 工具类 │ └── constants.dart └── main.dart # 应用入口3. API接口实现
3.1 定义数据模型
根据和风天气API文档,我们需要先定义返回数据的模型类。以获取城市天气为例:
@JsonSerializable() class WeatherResponse { final String code; final String updateTime; final String fxLink; final Now now; WeatherResponse({ required this.code, required this.updateTime, required this.fxLink, required this.now, }); factory WeatherResponse.fromJson(Map<String, dynamic> json) => _$WeatherResponseFromJson(json); Map<String, dynamic> toJson() => _$WeatherResponseToJson(this); } @JsonSerializable() class Now { final String obsTime; final String temp; final String feelsLike; final String icon; final String text; final String wind360; final String windDir; final String windScale; final String windSpeed; final String humidity; final String precip; final String pressure; final String vis; final String cloud; final String dew; Now({ required this.obsTime, required this.temp, required this.feelsLike, // 其他字段... }); factory Now.fromJson(Map<String, dynamic> json) => _$NowFromJson(json); Map<String, dynamic> toJson() => _$NowToJson(this); }3.2 使用retrofit定义API接口
创建weather_api.dart文件,使用retrofit定义接口:
import 'package:retrofit/retrofit.dart'; import 'package:dio/dio.dart'; part 'weather_api.g.dart'; @RestApi(baseUrl: "https://devapi.qweather.com/v7/") abstract class WeatherApi { factory WeatherApi(Dio dio, {String baseUrl}) = _WeatherApi; @GET("weather/now") Future<WeatherResponse> getWeatherNow({ @Query("location") required String location, @Query("key") required String key, @Query("lang") String lang = "zh", @Query("unit") String unit = "m", }); }运行以下命令生成代码:
flutter pub run build_runner build4. 天气卡片UI实现
4.1 基础卡片布局
在weather_card.dart中创建WeatherCard组件:
class WeatherCard extends StatelessWidget { final WeatherResponse weather; const WeatherCard({super.key, required this.weather}); @override Widget build(BuildContext context) { return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), const SizedBox(height: 16), _buildWeatherInfo(), const SizedBox(height: 16), _buildExtraInfo(), ], ), ), ); } Widget _buildHeader() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( weather.now.text, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), Image.network( "https://a.hecdn.net/img/common/icon/202106d/${weather.now.icon}.png", width: 48, height: 48, ), ], ); } // 其他构建方法... }4.2 响应式设计考虑
为了让卡片在不同设备上都有良好表现,我们需要添加响应式设计:
LayoutBuilder( builder: (context, constraints) { final isSmallScreen = constraints.maxWidth < 400; return Flex( direction: isSmallScreen ? Axis.vertical : Axis.horizontal, children: [ // 根据屏幕尺寸调整布局 ], ); }, )5. 数据获取与状态管理
5.1 实现数据获取逻辑
创建一个WeatherRepository类封装API调用:
class WeatherRepository { final WeatherApi _api; WeatherRepository({required String apiKey}) : _api = WeatherApi(Dio(), baseUrl: "https://devapi.qweather.com/v7/"); Future<WeatherResponse> getWeather(String location) async { try { final response = await _api.getWeatherNow( location: location, key: const String.fromEnvironment("WEATHER_API_KEY"), ); if (response.code != "200") { throw Exception("API Error: ${response.code}"); } return response; } on DioException catch (e) { throw Exception("Network Error: ${e.message}"); } } }5.2 状态管理方案选择
对于简单的天气卡片,使用StatefulWidget或Provider就足够了:
class WeatherCardContainer extends StatefulWidget { final String location; const WeatherCardContainer({super.key, required this.location}); @override State<WeatherCardContainer> createState() => _WeatherCardContainerState(); } class _WeatherCardContainerState extends State<WeatherCardContainer> { late final WeatherRepository _repository; WeatherResponse? _weather; String? _error; @override void initState() { super.initState(); _repository = WeatherRepository( apiKey: const String.fromEnvironment("WEATHER_API_KEY"), ); _loadWeather(); } Future<void> _loadWeather() async { try { final weather = await _repository.getWeather(widget.location); setState(() { _weather = weather; _error = null; }); } catch (e) { setState(() { _error = e.toString(); }); } } @override Widget build(BuildContext context) { if (_error != null) { return ErrorWidget(_error!); } if (_weather == null) { return const Center(child: CircularProgressIndicator()); } return WeatherCard(weather: _weather!); } }6. 鸿蒙系统适配要点
6.1 平台特性处理
虽然Flutter是跨平台的,但鸿蒙系统还是有些特性需要注意:
import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/material.dart' show TargetPlatform; // 检测是否为鸿蒙系统 bool get isHarmonyOS { return defaultTargetPlatform == TargetPlatform.android && Platform.environment.containsKey("HARMONY_OS"); }6.2 性能优化建议
鸿蒙系统对Flutter应用的性能要求较高,建议:
- 使用
const构造函数尽可能多的地方 - 对网络图片使用cached_network_image插件
- 避免在build方法中做耗时操作
- 使用
ListView.builder而不是Column+List处理长列表
7. 测试与调试技巧
7.1 单元测试示例
为API接口编写测试:
void main() { late WeatherRepository repository; setUp(() { repository = WeatherRepository(apiKey: "test_key"); }); test('getWeather returns valid data', () async { final weather = await repository.getWeather("101010100"); expect(weather.code, "200"); expect(weather.now.temp, isNotNull); }); }7.2 调试网络请求
使用dio的拦截器记录请求日志:
final dio = Dio() ..interceptors.add(LogInterceptor( request: true, requestHeader: true, requestBody: true, responseHeader: true, responseBody: true, error: true, ));8. 项目优化方向
8.1 缓存策略实现
减少API调用次数,实现简单的内存缓存:
class CachedWeatherRepository { final WeatherRepository _delegate; final Map<String, WeatherResponse> _cache = {}; final Duration _cacheDuration; CachedWeatherRepository({ required WeatherRepository delegate, Duration cacheDuration = const Duration(minutes: 30), }) : _delegate = delegate, _cacheDuration = cacheDuration; Future<WeatherResponse> getWeather(String location) async { final cached = _cache[location]; if (cached != null && DateTime.now().difference(cached.updateTime) < _cacheDuration) { return cached; } final fresh = await _delegate.getWeather(location); _cache[location] = fresh; return fresh; } }8.2 国际化支持
添加多语言支持:
class WeatherCard extends StatelessWidget { // ... Widget _buildTemperature(BuildContext context) { final unit = Localizations.localeOf(context).languageCode == 'en' ? '°F' : '°C'; return Text( '${weather.now.temp}$unit', style: TextStyle(fontSize: 24), ); } }9. 常见问题与解决方案
9.1 API返回错误代码处理
和风天气常见的错误代码及处理方式:
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| 204 | 无数据 | 检查location参数是否正确 |
| 401 | 认证失败 | 检查API Key是否有效 |
| 404 | 无效请求 | 检查API地址和参数 |
| 500 | 服务器错误 | 稍后重试 |
9.2 网络请求超时设置
为Dio配置合理的超时时间:
final dio = Dio(BaseOptions( connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 3), ));10. 项目部署与发布
10.1 环境变量配置
安全地管理API Key:
# --dart-define=WEATHER_API_KEY=your_api_key flutter run --dart-define=WEATHER_API_KEY=your_api_key10.2 鸿蒙应用打包
虽然Flutter应用可以直接在鸿蒙设备上运行,但正式发布需要:
- 按照鸿蒙应用规范配置应用信息
- 添加鸿蒙特有的权限声明
- 使用鸿蒙的签名工具对应用进行签名
- 提交到华为应用市场审核
提示:鸿蒙系统对应用权限管理较严格,确保只申请必要的权限