dart学习第 24 节:核心特性实战 —— 天气 API 数据解析

上一节课我们学习了单元测试,掌握了通过全面测试保证代码质量的方法。今天,我们将进入第 24 课 ------核心特性实战:天气 API 数据解析。通过这个实战项目,我们会综合运用 Dart 的多个核心特性,包括 HTTP 请求、异步编程、JSON 解析和空安全处理,打造一个能获取并展示天气数据的小应用。

一、项目需求

我们的目标是创建一个 Dart 应用程序,它能够做到以下几点:

  1. 调用公共 API:我们将使用 Open - Meteo API。它的优势在于,对于非商业用途,无需注册获取 API 密钥,可直接使用,能轻松获取天气数据。
  2. 获取天气数据:我们希望检索特定位置的当前天气信息。位置可以通过地理坐标(纬度和经度)或者在某些情况下通过城市名称来指定。
  3. 解析 JSON 数据:API 返回的数据采用 JSON 格式。我们需要将此 JSON 数据转换为 Dart 对象,以便在应用程序中方便地处理。
  4. 展示天气信息:解析数据后,我们将以有意义的方式展示相关天气信息,如温度、天气描述、湿度等。

二、技术要点

  1. Dart 中的 HTTP 请求

    • 在 Dart 中,有多种发送 HTTP 请求的方式。最常用的方式之一是使用http包。首先,我们需要在pubspec.yaml文件中添加它作为依赖:
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函数getWeatherDataFuture关键字表明此函数是异步的,会在未来某个时刻返回结果。
    • 使用Uri.https构建 API 请求的 URL。传入基础 URL、API 端点(这里是/v1/forecast)以及查询参数,如纬度、经度和是否获取当前天气的标识。
    • 使用http.get方法发送 GET 请求到 API。由于这是一个异步操作,使用await关键字。await会暂停函数的执行,直到http.get操作完成并返回一个response
    • 检查响应的statusCode是否为 200(表示请求成功)。如果是,返回响应体(即包含 JSON 格式天气数据的字符串)。否则,抛出一个Exception表示请求失败。
  1. 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完成,我们打印结果,然后再打印另一条消息。
  1. 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类中:

    • 定义了两个最终属性temperaturedescription,用于存储相关的天气信息。
    • 构造函数接受这两个属性作为必需参数。
    • 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 响应可能并不总是包含我们期望的所有数据,或者可能由于网络问题导致返回空响应。

  1. 网络数据的空值处理

    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
  1. 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块中打印错误消息。

五、进一步改进

  1. 错误日志记录

    目前,我们只是简单地打印错误消息。在实际应用中,将错误记录到文件或日志服务中会更好。这样,我们可以在以后查看错误并更有效地进行调试。例如,可以使用logging包来实现更高级的日志记录功能。

  2. 用户输入功能

    现在我们是硬编码了纬度和经度,我们可以通过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('请输入有效的纬度和经度');
  }
}
  1. 用户界面集成

    为了获得更友好的用户体验,我们可以将这个天气数据检索功能集成到图形用户界面中。如果使用 Flutter,我们可以创建一个漂亮的应用程序,使用各种小部件以有序且视觉吸引人的方式显示天气信息。例如,使用ListView来展示不同的天气信息字段,使用Image.network来显示与天气状况对应的图标等。

相关推荐
程序员老刘2 小时前
uni-app X能成为下一个Flutter吗?
flutter·uni-app·客户端
叽哥3 小时前
flutter学习第 2 节:第一个 Flutter 应用
android·flutter
TralyFang9 小时前
Flutter CachedNetworkImage 的解码、缩放和缓存策略
flutter
勤劳打代码9 小时前
抽丝剥茧 —— 解析 PC 蓝牙检测
c++·flutter·客户端
ilmari9 小时前
HarmonyOS 基于Network Kit封装的网络请求工具
android·flutter·harmonyos
来来走走19 小时前
Flutter开发 了解Scaffold
android·开发语言·flutter
zeqinjie1 天前
Flutter 使用 AI Cursor 快速完成一个图表封装【提效】
前端·flutter
叽哥1 天前
dart学习第 23 节: 单元测试入门 —— 保证代码质量
flutter·dart
一念之间lq1 天前
学习Flutter-Flutter项目如何运行
flutter