上一节课我们学习了单元测试,掌握了通过全面测试保证代码质量的方法。今天,我们将进入第 24 课 ------核心特性实战:天气 API 数据解析。通过这个实战项目,我们会综合运用 Dart 的多个核心特性,包括 HTTP 请求、异步编程、JSON 解析和空安全处理,打造一个能获取并展示天气数据的小应用。
一、项目需求
我们的目标是创建一个 Dart 应用程序,它能够做到以下几点:
- 调用公共 API:我们将使用 Open - Meteo API。它的优势在于,对于非商业用途,无需注册获取 API 密钥,可直接使用,能轻松获取天气数据。
- 获取天气数据:我们希望检索特定位置的当前天气信息。位置可以通过地理坐标(纬度和经度)或者在某些情况下通过城市名称来指定。
- 解析 JSON 数据:API 返回的数据采用 JSON 格式。我们需要将此 JSON 数据转换为 Dart 对象,以便在应用程序中方便地处理。
- 展示天气信息:解析数据后,我们将以有意义的方式展示相关天气信息,如温度、天气描述、湿度等。
二、技术要点
-
Dart 中的 HTTP 请求
- 在 Dart 中,有多种发送 HTTP 请求的方式。最常用的方式之一是使用
http
包。首先,我们需要在pubspec.yaml
文件中添加它作为依赖:
- 在 Dart 中,有多种发送 HTTP 请求的方式。最常用的方式之一是使用
yaml
dependencies:
http: ^1.4.0
然后,运行dart pub get
安装该包。
- 以下是一个向 Open - Meteo API 发送简单 GET 请求以获取特定位置天气数据的示例。假设我们要获取某个具有特定坐标位置的天气。
dart
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<String> getWeatherData(double latitude, double longitude) async {
final url = Uri.https('api.open-meteo.com', 'v1/forecast', {
'latitude': latitude.toString(),
'longitude': longitude.toString(),
'current_weather': 'true',
});
final response = await http.get(url);
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('获取天气数据失败');
}
}
-
在这段代码中:
- 我们首先导入
http
包和dart:convert
库,后续解析 JSON 时会用到后者。 - 定义了一个
Future
函数getWeatherData
。Future
关键字表明此函数是异步的,会在未来某个时刻返回结果。 - 使用
Uri.https
构建 API 请求的 URL。传入基础 URL、API 端点(这里是/v1/forecast
)以及查询参数,如纬度、经度和是否获取当前天气的标识。 - 使用
http.get
方法发送 GET 请求到 API。由于这是一个异步操作,使用await
关键字。await
会暂停函数的执行,直到http.get
操作完成并返回一个response
。 - 检查响应的
statusCode
是否为 200(表示请求成功)。如果是,返回响应体(即包含 JSON 格式天气数据的字符串)。否则,抛出一个Exception
表示请求失败。
- 我们首先导入
- Future 与异步编程
- 在 Dart 中,异步操作不会立即完成。例如,HTTP 请求由于需要通过网络与服务器通信,需要一些时间来完成。Dart 使用
Future
来表示异步操作的结果,而不是在等待请求完成时阻塞程序的执行。 - 在上述
getWeatherData
函数中,函数本身返回一个Future<String>
。这意味着函数的结果(即 JSON 格式的天气数据字符串)将在未来可用。 - 当在
async
函数内部使用await
关键字时,我们是在告诉 Dart 等待Future
完成后再继续执行函数的其余部分。例如,在getWeatherData
函数中,await http.get(url)
语句会暂停函数,直到 HTTP 请求完成,然后将结果赋值给response
变量。 - 如果不使用
await
,直接返回http.get(url)
而不等待它,函数会立即返回一个未完成的Future
。这不是我们想要的,因为我们需要实际的天气数据来进行后续的解析和展示。 - 这里有另一个示例来说明
Future
的工作原理。假设我们有一个函数模拟延迟后返回一个值:
dart
Future<int> delayAndReturn() async {
await Future.delayed(Duration(seconds: 2));
return 42;
}
void main() async {
print('在调用delayAndReturn之前');
int result = await delayAndReturn();
print('结果是:$result');
print('在调用delayAndReturn之后');
}
- 在这段代码中,
delayAndReturn
函数使用Future.delayed
暂停 2 秒,然后返回值 42。在main
函数中,我们首先打印一条消息,然后await
delayAndReturn
的结果。在等待结果的过程中,程序不会阻塞,如果有其他代码,它们可以继续执行。一旦delayAndReturn
中的Future
完成,我们打印结果,然后再打印另一条消息。
-
JSON 到模型的转换
从 API 获取到 JSON 格式的天气数据后,我们需要将其转换为 Dart 对象以便更方便地操作。首先,定义一个 Dart 类来表示天气数据。为简单起见,考虑一个基本的
Weather
类,它包含温度和天气描述:
dart
class Weather {
final double temperature;
final String description;
Weather({required this.temperature, required this.description});
factory Weather.fromJson(Map<String, dynamic> json) {
double temp = json['current_weather']['temperature'] ?? 0.0;
String desc = json['current_weather']['weathercode'] != null
? getWeatherDescription(json['current_weather']['weathercode'])
: '未知';
return Weather(temperature: temp, description: desc);
}
// 辅助函数,根据天气代码获取天气描述
static String getWeatherDescription(int weathercode) {
// 这里可以根据Open - Meteo的天气代码表进行映射
// 例如:
switch (weathercode) {
case 0:
return '晴朗';
case 1:
return '大部分晴朗';
// 其他代码的映射...
default:
return '未知';
}
}
}
-
在这个
Weather
类中:- 定义了两个最终属性
temperature
和description
,用于存储相关的天气信息。 - 构造函数接受这两个属性作为必需参数。
fromJson
工厂构造函数用于从 JSON 映射创建一个Weather
对象。在 Open - Meteo API 响应中,温度位于current_weather
对象内,天气代码也在current_weather
中。我们使用空感知运算符(??
)为可能缺失的字段提供默认值。对于天气描述,通过调用辅助函数getWeatherDescription
,根据天气代码获取对应的文字描述。
- 定义了两个最终属性
-
现在,我们可以修改
getWeatherData
函数,使其返回一个Weather
对象,而不是 JSON 字符串:
dart
Future<Weather> getWeatherData(double latitude, double longitude) async {
final url = Uri.https('api.open - meteo.com', 'v1/forecast', {
'latitude': latitude.toString(),
'longitude': longitude.toString(),
'current_weather': 'true',
});
final response = await http.get(url);
if (response.statusCode == 200) {
Map<String, dynamic> json = jsonDecode(response.body);
return Weather.fromJson(json);
} else {
throw Exception('获取天气数据失败');
}
}
- 这里,我们使用
dart:convert
库中的jsonDecode
函数将response.body
中的 JSON 字符串转换为 Dart 的Map<String, dynamic>
。然后将这个映射传递给Weather
类的fromJson
工厂构造函数,创建一个Weather
对象。
三、空安全处理
在使用像 Open - Meteo 这样的外部 API 时,处理空值非常重要。API 响应可能并不总是包含我们期望的所有数据,或者可能由于网络问题导致返回空响应。
-
网络数据的空值处理
在
getWeatherData
函数中,我们已经有了一些基本的空安全处理。当 HTTP 请求失败(statusCode
不是 200)时,我们抛出一个Exception
,而不是返回可能为空或不正确的结果。然而,我们还可以添加更详细的错误处理。例如,如果 API 返回的数据格式不正确,我们可以进一步处理。在
fromJson
工厂构造函数中,我们可以增加对数据完整性的检查:
dart
factory Weather.fromJson(Map<String, dynamic> json) {
if (!json.containsKey('current_weather')) {
throw Exception('API返回的数据格式不正确,缺少current_weather字段');
}
double temp = json['current_weather']['temperature']?? 0.0;
String desc = json['current_weather']['weathercode']!= null
? getWeatherDescription(json['current_weather']['weathercode'])
: '未知';
return Weather(temperature: temp, description: desc);
}
- 在这个更新后的
fromJson
构造函数中,我们首先检查 JSON 映射是否包含current_weather
键。如果不包含,说明数据格式有问题,抛出一个Exception
。
-
JSON 解析中的空感知运算符
如在
fromJson
构造函数中所见,我们在从 JSON 映射提取值时使用了空感知运算符(??
)。例如,double temp = json['current_weather']['temperature']?? 0.0;
。这确保了如果current_weather
对象中不存在temperature
键,我们使用默认值0.0
,而不是得到一个可能导致运行时错误的空值。类似地,对于天气描述,通过检查天气代码是否存在来决定是获取描述还是使用默认值
未知
。这种方式有效地避免了因数据缺失而导致的程序崩溃。
四、整合代码
让我们创建一个简单的基于控制台的 Dart 应用程序,展示所有这些概念如何协同工作。
dart
import 'package:http/http.dart' as http;
import 'dart:convert';
class Weather {
final double temperature;
final String description;
Weather({required this.temperature, required this.description});
factory Weather.fromJson(Map<String, dynamic> json) {
if (!json.containsKey('current_weather')) {
throw Exception('API返回的数据格式不正确,缺少current_weather字段');
}
double temp = json['current_weather']['temperature'] ?? 0.0;
String desc = json['current_weather']['weathercode'] != null
? getWeatherDescription(json['current_weather']['weathercode'])
: '未知';
return Weather(temperature: temp, description: desc);
}
static String getWeatherDescription(int weathercode) {
switch (weathercode) {
case 0:
return '晴朗';
case 1:
return '大部分晴朗';
// 其他代码的映射...
default:
return '未知';
}
}
}
Future<Weather> getWeatherData(double latitude, double longitude) async {
final url = Uri.https('api.open-meteo.com', 'v1/forecast', {
'latitude': latitude.toString(),
'longitude': longitude.toString(),
'current_weather': 'true',
});
final response = await http.get(url);
if (response.statusCode == 200) {
Map<String, dynamic> json = jsonDecode(response.body);
return Weather.fromJson(json);
} else {
throw Exception('获取天气数据失败');
}
}
void main() async {
double latitude = 34.0522; // 示例纬度,可替换为你需要的
double longitude = -118.2437; // 示例经度,可替换为你需要的
try {
Weather weather = await getWeatherData(latitude, longitude);
print('该位置的天气是:${weather.description}');
print('温度是:${weather.temperature} °C');
} catch (e) {
print('错误:$e');
}
}
在这个main
函数中:
- 我们首先定义了示例的纬度和经度。在实际应用中,你可以根据需要修改这些值,或者通过用户输入获取。
- 使用
try - catch
块处理在 API 调用或 JSON 解析过程中可能出现的任何异常。 - 在
try
块内,await
getWeatherData
函数的结果。如果操作成功,打印天气描述和温度。 - 如果发生异常(如网络错误、API 返回的数据格式错误等),在
catch
块中打印错误消息。
五、进一步改进
-
错误日志记录
目前,我们只是简单地打印错误消息。在实际应用中,将错误记录到文件或日志服务中会更好。这样,我们可以在以后查看错误并更有效地进行调试。例如,可以使用
logging
包来实现更高级的日志记录功能。 -
用户输入功能
现在我们是硬编码了纬度和经度,我们可以通过
dart:io
库实现用户输入功能,让用户输入城市名称或坐标。然后根据用户输入来查询天气。例如:
dart
import 'dart:io';
void main() async {
stdout.write('请输入纬度:');
String? latitudeStr = stdin.readLineSync();
stdout.write('请输入经度:');
String? longitudeStr = stdin.readLineSync();
if (latitudeStr != null && longitudeStr != null) {
double latitude = double.tryParse(latitudeStr) ?? 0.0;
double longitude = double.tryParse(longitudeStr) ?? 0.0;
try {
Weather weather = await getWeatherData(latitude, longitude);
print('该位置的天气是:${weather.description}');
print('温度是:${weather.temperature} °C');
} catch (e) {
print('错误:$e');
}
} else {
print('请输入有效的纬度和经度');
}
}
-
用户界面集成
为了获得更友好的用户体验,我们可以将这个天气数据检索功能集成到图形用户界面中。如果使用 Flutter,我们可以创建一个漂亮的应用程序,使用各种小部件以有序且视觉吸引人的方式显示天气信息。例如,使用
ListView
来展示不同的天气信息字段,使用Image.network
来显示与天气状况对应的图标等。