在这里插入图片描述
在这里插入图片描述

引言

在Flutter应用开发中,数据模型是连接JSON数据和业务逻辑的桥梁。良好的数据模型设计不仅能提高代码的可读性和可维护性,还能确保类型安全和数据完整性。特别是在天气查询应用中,数据模型需要处理复杂的嵌套结构、可选字段和各种数据类型。

本章节将深入探讨JSON数据模型设计的核心原则和最佳实践,结合天气查询应用的实际场景,帮助读者掌握设计高质量数据模型的方法。

1. 数据模型设计原则

在开始设计数据模型之前,需要明确一些基本原则:

1.1 单一职责原则

每个数据模型类应该只负责一个实体的数据:

// 正确:每个类负责一个实体
class Weather {
  final String city;
  final double temperature;
  
  Weather({required this.city, required this.temperature});
}

class Wind {
  final String direction;
  final double speed;
  
  Wind({required this.direction, required this.speed});
}

// 错误:一个类负责多个实体
class WeatherAndWind {
  final String city;
  final double temperature;
  final String windDirection;
  final double windSpeed;
}

1.2 不可变性原则

使用final字段确保数据不可变,避免意外的数据修改:

// 正确:所有字段都是final
class Weather {
  final String city;
  final double temperature;
  final DateTime lastUpdated;
  
  Weather({
    required this.city,
    required this.temperature,
    required this.lastUpdated,
  });
}

// 错误:字段可以被修改
class MutableWeather {
  String city = "";
  double temperature = 0.0;
  
  void updateTemperature(double newTemp) {
    temperature = newTemp;
  }
}

1.3 类型安全原则

明确指定字段类型,避免使用dynamic

// 正确:明确的类型
class Weather {
  final String city;
  final double temperature;
  final int humidity;
  
  Weather({
    required this.city,
    required this.temperature,
    required this.humidity,
  });
}

// 错误:使用dynamic
class DynamicWeather {
  dynamic city;
  dynamic temperature;
  dynamic humidity;
}

1.4 可扩展性原则

考虑未来可能的字段变更,合理设计模型结构:

class Weather {
  final String city;
  final double temperature;
  final String condition;
  final int humidity;
  final double windSpeed;
  final String description;
  final DateTime lastUpdated;
  
  Weather({
    required this.city,
    required this.temperature,
    required this.condition,
    required this.humidity,
    required this.windSpeed,
    this.description = "",
    required this.lastUpdated,
  });
}

1.5 错误处理原则

处理可选字段和类型转换错误:

class SafeWeather {
  final String city;
  final double temperature;
  final String condition;
  
  SafeWeather._({
    required this.city,
    required this.temperature,
    required this.condition,
  });
  
  factory SafeWeather.fromJson(Map<String, dynamic> json) {
    return SafeWeather._(
      city: _parseString(json["city"]),
      temperature: _parseDouble(json["temperature"]),
      condition: _parseString(json["condition"]),
    );
  }
}

String _parseString(dynamic value) {
  if (value is String) return value;
  return "";
}

double _parseDouble(dynamic value) {
  if (value is num) return value.toDouble();
  return 0.0;
}

2. 标准数据模型设计

标准数据模型是最常用的设计方式,包含fromJson构造函数和toJson方法。

2.1 基础模型设计

class Weather {
  final String city;
  final double temperature;
  final String condition;
  final int humidity;
  final double windSpeed;
  final String description;
  final DateTime lastUpdated;
  
  Weather({
    required this.city,
    required this.temperature,
    required this.condition,
    required this.humidity,
    required this.windSpeed,
    this.description = "",
    required this.lastUpdated,
  });
  
  factory Weather.fromJson(Map<String, dynamic> json) {
    return Weather(
      city: json["city"] as String,
      temperature: (json["temperature"] as num).toDouble(),
      condition: json["condition"] as String,
      humidity: json["humidity"] as int,
      windSpeed: (json["wind_speed"] as num?)?.toDouble() ?? 0.0,
      description: json["description"] as String? ?? "",
      lastUpdated: DateTime.parse(json["last_updated"] as String),
    );
  }
  
  Map<String, dynamic> toJson() {
    return {
      "city": city,
      "temperature": temperature,
      "condition": condition,
      "humidity": humidity,
      "wind_speed": windSpeed,
      "description": description,
      "last_updated": lastUpdated.toIso8601String(),
    };
  }
}

2.2 模型组合

当JSON结构包含嵌套对象时,需要组合多个模型:

class Wind {
  final String direction;
  final double speed;
  
  Wind({required this.direction, required this.speed});
  
  factory Wind.fromJson(Map<String, dynamic> json) {
    return Wind(
      direction: json["direction"] as String,
      speed: (json["speed"] as num).toDouble(),
    );
  }
  
  Map<String, dynamic> toJson() {
    return {"direction": direction, "speed": speed};
  }
}

class CurrentWeather {
  final double temp;
  final int humidity;
  final String condition;
  final Wind wind;
  final String updateTime;
  
  CurrentWeather({
    required this.temp,
    required this.humidity,
    required this.condition,
    required this.wind,
    required this.updateTime,
  });
  
  factory CurrentWeather.fromJson(Map<String, dynamic> json) {
    return CurrentWeather(
      temp: (json["temp"] as num).toDouble(),
      humidity: json["humidity"] as int,
      condition: json["condition"] as String,
      wind: Wind.fromJson(json["wind"]),
      updateTime: json["updateTime"] as String,
    );
  }
}

class WeatherResponse {
  final CurrentWeather current;
  final List<DailyForecast> forecast;
  final DateTime lastUpdated;
  
  WeatherResponse({
    required this.current,
    required this.forecast,
    required this.lastUpdated,
  });
  
  factory WeatherResponse.fromJson(Map<String, dynamic> json) {
    List<dynamic> forecastList = json["forecast"] as List;
    
    return WeatherResponse(
      current: CurrentWeather.fromJson(json["current"]),
      forecast: forecastList
          .map((e) => DailyForecast.fromJson(e))
          .toList(),
      lastUpdated: DateTime.parse(json["last_updated"] as String),
    );
  }
}

2.3 数组处理

处理JSON中的数组数据:

class DailyForecast {
  final String date;
  final int high;
  final int low;
  final String condition;
  
  DailyForecast({
    required this.date,
    required this.high,
    required this.low,
    required this.condition,
  });
  
  factory DailyForecast.fromJson(Map<String, dynamic> json) {
    return DailyForecast(
      date: json["date"] as String,
      high: json["high"] as int,
      low: json["low"] as int,
      condition: json["condition"] as String,
    );
  }
}

class HourlyForecast {
  final String time;
  final int temp;
  
  HourlyForecast({required this.time, required this.temp});
  
  factory HourlyForecast.fromJson(Map<String, dynamic> json) {
    return HourlyForecast(
      time: json["time"] as String,
      temp: json["temp"] as int,
    );
  }
}

3. 使用freezed实现不可变模型

freezed是一个强大的代码生成库,可以帮助我们实现不可变数据模型。

3.1 添加依赖

首先需要在pubspec.yaml中添加依赖:

dependencies:
  freezed_annotation: ^2.4.4
  json_annotation: ^4.9.0

dev_dependencies:
  build_runner: ^2.4.8
  freezed: ^2.5.7
  json_serializable: ^6.8.0

3.2 定义freezed模型

import 'package:freezed_annotation/freezed_annotation.dart';

part 'weather.freezed.dart';
part 'weather.g.dart';


class Weather with _$Weather {
  const factory Weather({
    required String city,
    required double temperature,
    required String condition,
    required int humidity,
    (0.0) double windSpeed,
    ("") String description,
    required DateTime lastUpdated,
  }) = _Weather;
  
  factory Weather.fromJson(Map<String, dynamic> json) =>
      _$WeatherFromJson(json);
}


class Wind with _$Wind {
  const factory Wind({
    required String direction,
    required double speed,
  }) = _Wind;
  
  factory Wind.fromJson(Map<String, dynamic> json) =>
      _$WindFromJson(json);
}

3.3 freezed的优势

特性 手动实现 freezed
不可变性 需要手动声明final 自动实现
相等性判断 需要重写==和hashCode 自动生成
copyWith 需要手动实现 自动生成
toString 默认实现不友好 自动生成可读格式
模式匹配 不支持 支持when/maybeWhen
代码量

3.4 使用copyWith更新数据

void copyWithDemo() {
  Weather weather = Weather(
    city: "北京",
    temperature: 28.5,
    condition: "晴",
    humidity: 65,
    lastUpdated: DateTime.now(),
  );
  
  Weather updatedWeather = weather.copyWith(
    temperature: 30.0,
    condition: "多云",
  );
  
  print('原始温度: ${weather.temperature}');
  print('更新温度: ${updatedWeather.temperature}');
}

3.5 使用模式匹配

void patternMatchingDemo(Weather weather) {
  String status = weather.when(
    sunny: (weather) => "天气晴朗",
    rainy: (weather) => "正在下雨",
    cloudy: (weather) => "多云天气",
    snowy: (weather) => "雪花飘飘",
  );
  
  print(status);
}

4. 错误安全的模型设计

在实际开发中,JSON数据可能包含错误或缺失的字段,需要设计具有错误处理能力的数据模型。

4.1 安全解析函数

class JsonParser {
  static String parseString(dynamic value, {String defaultValue = ""}) {
    if (value is String) return value;
    return defaultValue;
  }
  
  static int parseInt(dynamic value, {int defaultValue = 0}) {
    if (value is int) return value;
    if (value is double) return value.toInt();
    if (value is String) return int.tryParse(value) ?? defaultValue;
    return defaultValue;
  }
  
  static double parseDouble(dynamic value, {double defaultValue = 0.0}) {
    if (value is double) return value;
    if (value is int) return value.toDouble();
    if (value is String) return double.tryParse(value) ?? defaultValue;
    return defaultValue;
  }
  
  static bool parseBool(dynamic value, {bool defaultValue = false}) {
    if (value is bool) return value;
    if (value is String) return value.toLowerCase() == "true";
    if (value is int) return value == 1;
    return defaultValue;
  }
  
  static DateTime? parseDateTime(dynamic value) {
    if (value is String) {
      return DateTime.tryParse(value);
    }
    if (value is int) {
      return DateTime.fromMillisecondsSinceEpoch(value);
    }
    return null;
  }
}

4.2 使用安全解析函数

class SafeWeather {
  final String city;
  final double temperature;
  final String condition;
  final int humidity;
  final DateTime? lastUpdated;
  
  SafeWeather({
    required this.city,
    required this.temperature,
    required this.condition,
    required this.humidity,
    this.lastUpdated,
  });
  
  factory SafeWeather.fromJson(Map<String, dynamic> json) {
    return SafeWeather(
      city: JsonParser.parseString(json["city"]),
      temperature: JsonParser.parseDouble(json["temperature"]),
      condition: JsonParser.parseString(json["condition"]),
      humidity: JsonParser.parseInt(json["humidity"]),
      lastUpdated: JsonParser.parseDateTime(json["last_updated"]),
    );
  }
}

4.3 验证数据完整性

class ValidatedWeather {
  final String city;
  final double temperature;
  final String condition;
  
  ValidatedWeather._({
    required this.city,
    required this.temperature,
    required this.condition,
  });
  
  static ValidatedWeather? fromJson(Map<String, dynamic> json) {
    String? city = JsonParser.parseString(json["city"]);
    if (city == null || city.isEmpty) {
      print("城市名称无效");
      return null;
    }
    
    double temperature = JsonParser.parseDouble(json["temperature"]);
    if (temperature < -100 || temperature > 100) {
      print("温度值超出范围");
      return null;
    }
    
    String? condition = JsonParser.parseString(json["condition"]);
    if (condition == null || condition.isEmpty) {
      print("天气状况无效");
      return null;
    }
    
    return ValidatedWeather._(
      city: city,
      temperature: temperature,
      condition: condition,
    );
  }
}

5. 模型扩展方法

使用扩展方法为模型添加额外的功能,保持模型类的简洁。

5.1 添加计算属性

extension WeatherExtension on Weather {
  String get temperatureStatus {
    if (temperature > 30) return "炎热";
    if (temperature > 20) return "舒适";
    if (temperature > 10) return "凉爽";
    return "寒冷";
  }
  
  bool get isSunny => condition == "晴" || condition == "sunny";
  
  bool get isRainy => condition == "雨" || condition == "rainy";
  
  String formatLastUpdated() {
    return "${lastUpdated.month}${lastUpdated.day}${lastUpdated.hour}:${lastUpdated.minute.toString().padLeft(2, "0")}";
  }
  
  String get conditionEmoji {
    switch (condition) {
      case "晴":
      case "sunny":
        return "☀️";
      case "多云":
      case "cloudy":
        return "⛅";
      case "雨":
      case "rainy":
        return "🌧️";
      case "雪":
      case "snowy":
        return "❄️";
      default:
        return "🌤️";
    }
  }
}

5.2 使用扩展方法

void extensionUsage() {
  Weather weather = Weather(
    city: "北京",
    temperature: 28.5,
    condition: "晴",
    humidity: 65,
    lastUpdated: DateTime.now(),
  );
  
  print('${weather.city}的温度状态: ${weather.temperatureStatus}');
  print('是否晴天: ${weather.isSunny}');
  print('更新时间: ${weather.formatLastUpdated()}');
  print('天气图标: ${weather.conditionEmoji}');
}

5.3 添加转换方法

extension WeatherConversion on Weather {
  Map<String, dynamic> toSimpleJson() {
    return {
      "city": city,
      "temperature": temperature,
      "condition": condition,
    };
  }
  
  Weather copyWithTemp(double newTemp) {
    return Weather(
      city: city,
      temperature: newTemp,
      condition: condition,
      humidity: humidity,
      windSpeed: windSpeed,
      description: description,
      lastUpdated: lastUpdated,
    );
  }
}

6. 泛型数据模型

使用泛型可以创建通用的数据模型,避免重复代码。

6.1 创建泛型响应模型

class ApiResponse<T> {
  final int code;
  final String message;
  final T? data;
  
  ApiResponse({
    required this.code,
    required this.message,
    this.data,
  });
  
  factory ApiResponse.fromJson(
    Map<String, dynamic> json,
    T Function(Map<String, dynamic>) fromJsonT,
  ) {
    return ApiResponse<T>(
      code: json["code"] as int,
      message: json["message"] as String? ?? "",
      data: json["data"] != null ? fromJsonT(json["data"]) : null,
    );
  }
  
  bool get isSuccess => code == 200;
}

6.2 使用泛型响应模型

void genericResponseDemo() {
  String jsonStr = '''{
    "code": 200,
    "message": "success",
    "data": {"city": "北京", "temperature": 28.5}
  }''';
  
  Map<String, dynamic> data = json.decode(jsonStr);
  ApiResponse<Weather> response = ApiResponse.fromJson(
    data,
    (json) => Weather.fromJson(json),
  );
  
  if (response.isSuccess && response.data != null) {
    print('城市: ${response.data!.city}');
    print('温度: ${response.data!.temperature}');
  } else {
    print('请求失败: ${response.message}');
  }
}

6.3 创建泛型列表响应模型

class ApiListResponse<T> {
  final int code;
  final String message;
  final List<T>? data;
  
  ApiListResponse({
    required this.code,
    required this.message,
    this.data,
  });
  
  factory ApiListResponse.fromJson(
    Map<String, dynamic> json,
    T Function(Map<String, dynamic>) fromJsonT,
  ) {
    List<dynamic>? dataList = json["data"] as List?;
    
    return ApiListResponse<T>(
      code: json["code"] as int,
      message: json["message"] as String? ?? "",
      data: dataList?.map((e) => fromJsonT(e)).toList(),
    );
  }
  
  bool get isSuccess => code == 200;
  
  int get count => data?.length ?? 0;
}

7. 数据模型版本管理

随着API的迭代,数据模型可能需要进行版本管理,确保向后兼容。

7.1 版本字段

class Weather {
  final String city;
  final double temperature;
  final String condition;
  final int apiVersion;
  
  Weather({
    required this.city,
    required this.temperature,
    required this.condition,
    this.apiVersion = 1,
  });
  
  factory Weather.fromJson(Map<String, dynamic> json) {
    int version = json["api_version"] as int? ?? 1;
    
    if (version == 1) {
      return Weather(
        city: json["city"] as String,
        temperature: (json["temp"] as num).toDouble(),
        condition: json["condition"] as String,
        apiVersion: version,
      );
    } else {
      return Weather(
        city: json["city_name"] as String,
        temperature: (json["temperature"] as num).toDouble(),
        condition: json["weather_condition"] as String,
        apiVersion: version,
      );
    }
  }
}

7.2 迁移工具

class WeatherMigration {
  static Weather migrateFromV1(Map<String, dynamic> v1Data) {
    return Weather(
      city: v1Data["city"] as String,
      temperature: (v1Data["temp"] as num).toDouble(),
      condition: v1Data["condition"] as String,
      apiVersion: 1,
    );
  }
  
  static Weather migrateFromV2(Map<String, dynamic> v2Data) {
    return Weather(
      city: v2Data["city_name"] as String,
      temperature: (v2Data["temperature"] as num).toDouble(),
      condition: v2Data["weather_condition"] as String,
      apiVersion: 2,
    );
  }
  
  static Weather fromJson(Map<String, dynamic> json) {
    int version = json["api_version"] as int? ?? 1;
    
    if (version == 1) {
      return migrateFromV1(json);
    } else {
      return migrateFromV2(json);
    }
  }
}

8. 数据模型测试

为数据模型编写测试是确保代码质量的重要环节。

8.1 单元测试

import 'package:test/test.dart';
import 'dart:convert';

void main() {
  group('Weather.fromJson', () {
    test('should parse basic weather data', () {
      String jsonStr = '''{
        "city": "北京",
        "temperature": 28.5,
        "condition": "晴",
        "humidity": 65,
        "wind_speed": 3.5,
        "description": "晴朗天气",
        "last_updated": "2024-01-15T10:00:00Z"
      }''';
      
      Weather weather = Weather.fromJson(json.decode(jsonStr));
      
      expect(weather.city, equals("北京"));
      expect(weather.temperature, equals(28.5));
      expect(weather.condition, equals("晴"));
      expect(weather.humidity, equals(65));
      expect(weather.windSpeed, equals(3.5));
      expect(weather.description, equals("晴朗天气"));
    });
    
    test('should handle missing optional fields', () {
      String jsonStr = '''{
        "city": "北京",
        "temperature": 28.5,
        "condition": "晴",
        "humidity": 65,
        "last_updated": "2024-01-15T10:00:00Z"
      }''';
      
      Weather weather = Weather.fromJson(json.decode(jsonStr));
      
      expect(weather.windSpeed, equals(0.0));
      expect(weather.description, equals(""));
    });
    
    test('should handle null fields', () {
      String jsonStr = '''{
        "city": "北京",
        "temperature": 28.5,
        "condition": "晴",
        "humidity": 65,
        "wind_speed": null,
        "description": null,
        "last_updated": "2024-01-15T10:00:00Z"
      }''';
      
      Weather weather = Weather.fromJson(json.decode(jsonStr));
      
      expect(weather.windSpeed, equals(0.0));
      expect(weather.description, equals(""));
    });
  });
  
  group('Weather.toJson', () {
    test('should serialize weather data', () {
      Weather weather = Weather(
        city: "北京",
        temperature: 28.5,
        condition: "晴",
        humidity: 65,
        windSpeed: 3.5,
        description: "晴朗天气",
        lastUpdated: DateTime.parse("2024-01-15T10:00:00Z"),
      );
      
      Map<String, dynamic> json = weather.toJson();
      
      expect(json["city"], equals("北京"));
      expect(json["temperature"], equals(28.5));
      expect(json["condition"], equals("晴"));
      expect(json["wind_speed"], equals(3.5));
    });
  });
  
  group('WeatherExtension', () {
    test('temperatureStatus should return correct status', () {
      Weather hotWeather = Weather(
        city: "北京",
        temperature: 35.0,
        condition: "晴",
        humidity: 65,
        lastUpdated: DateTime.now(),
      );
      
      expect(hotWeather.temperatureStatus, equals("炎热"));
      
      Weather comfortableWeather = Weather(
        city: "北京",
        temperature: 25.0,
        condition: "晴",
        humidity: 65,
        lastUpdated: DateTime.now(),
      );
      
      expect(comfortableWeather.temperatureStatus, equals("舒适"));
    });
  });
}

8.2 属性测试

void fuzzTest() {
  for (int i = 0; i < 100; i++) {
    String jsonStr = generateRandomWeatherJson();
    
    try {
      Weather weather = Weather.fromJson(json.decode(jsonStr));
      
      expect(weather.city, isNotEmpty);
      expect(weather.temperature, isNotNull);
      expect(weather.condition, isNotEmpty);
    } catch (e) {
      print('测试用例失败: $jsonStr');
      print('错误信息: $e');
      rethrow;
    }
  }
}

String generateRandomWeatherJson() {
  List<String> cities = ["北京", "上海", "广州", "深圳", "杭州"];
  List<String> conditions = ["晴", "多云", "雨", "雪", "阴"];
  
  return '''{
    "city": "${cities[Random().nextInt(cities.length)]}",
    "temperature": ${10 + Random().nextDouble() * 30},
    "condition": "${conditions[Random().nextInt(conditions.length)]}",
    "humidity": ${30 + Random().nextInt(70)},
    "last_updated": "${DateTime.now().toIso8601String()}"
  }''';
}

9. 实战案例:天气查询应用完整数据模型

结合天气查询应用的实际场景,展示完整的数据模型设计:

class WeatherLocation {
  final String city;
  final String cityId;
  final double latitude;
  final double longitude;
  
  WeatherLocation({
    required this.city,
    required this.cityId,
    required this.latitude,
    required this.longitude,
  });
  
  factory WeatherLocation.fromJson(Map<String, dynamic> json) {
    return WeatherLocation(
      city: JsonParser.parseString(json["city"]),
      cityId: JsonParser.parseString(json["city_id"]),
      latitude: JsonParser.parseDouble(json["latitude"]),
      longitude: JsonParser.parseDouble(json["longitude"]),
    );
  }
}

class WeatherCondition {
  final String text;
  final String icon;
  final int code;
  
  WeatherCondition({
    required this.text,
    required this.icon,
    required this.code,
  });
  
  factory WeatherCondition.fromJson(Map<String, dynamic> json) {
    return WeatherCondition(
      text: JsonParser.parseString(json["text"]),
      icon: JsonParser.parseString(json["icon"]),
      code: JsonParser.parseInt(json["code"]),
    );
  }
}

class AirQuality {
  final int aqi;
  final String level;
  final String primary;
  
  AirQuality({
    required this.aqi,
    required this.level,
    required this.primary,
  });
  
  factory AirQuality.fromJson(Map<String, dynamic> json) {
    return AirQuality(
      aqi: JsonParser.parseInt(json["aqi"]),
      level: JsonParser.parseString(json["level"]),
      primary: JsonParser.parseString(json["primary"]),
    );
  }
  
  String get healthAdvice {
    if (aqi <= 50) return "空气质量优,适合户外活动";
    if (aqi <= 100) return "空气质量良,正常活动";
    if (aqi <= 150) return "轻度污染,敏感人群减少户外活动";
    if (aqi <= 200) return "中度污染,减少户外活动";
    if (aqi <= 300) return "重度污染,避免户外活动";
    return "严重污染,留在室内";
  }
}

class CurrentWeather {
  final WeatherLocation location;
  final WeatherCondition condition;
  final double temp;
  final double feelsLike;
  final int humidity;
  final int windDirection;
  final double windSpeed;
  final double visibility;
  final double pressure;
  final double uvIndex;
  final DateTime updateTime;
  
  CurrentWeather({
    required this.location,
    required this.condition,
    required this.temp,
    required this.feelsLike,
    required this.humidity,
    required this.windDirection,
    required this.windSpeed,
    required this.visibility,
    required this.pressure,
    required this.uvIndex,
    required this.updateTime,
  });
  
  factory CurrentWeather.fromJson(Map<String, dynamic> json) {
    return CurrentWeather(
      location: WeatherLocation.fromJson(json["location"]),
      condition: WeatherCondition.fromJson(json["condition"]),
      temp: JsonParser.parseDouble(json["temp"]),
      feelsLike: JsonParser.parseDouble(json["feels_like"]),
      humidity: JsonParser.parseInt(json["humidity"]),
      windDirection: JsonParser.parseInt(json["wind_direction"]),
      windSpeed: JsonParser.parseDouble(json["wind_speed"]),
      visibility: JsonParser.parseDouble(json["visibility"]),
      pressure: JsonParser.parseDouble(json["pressure"]),
      uvIndex: JsonParser.parseDouble(json["uv_index"]),
      updateTime: JsonParser.parseDateTime(json["update_time"]) ?? DateTime.now(),
    );
  }
}

class DailyForecast {
  final DateTime date;
  final WeatherCondition condition;
  final int high;
  final int low;
  final int humidity;
  final int windDirection;
  final double windSpeed;
  final int pop;
  final double uvIndex;
  
  DailyForecast({
    required this.date,
    required this.condition,
    required this.high,
    required this.low,
    required this.humidity,
    required this.windDirection,
    required this.windSpeed,
    required this.pop,
    required this.uvIndex,
  });
  
  factory DailyForecast.fromJson(Map<String, dynamic> json) {
    return DailyForecast(
      date: JsonParser.parseDateTime(json["date"]) ?? DateTime.now(),
      condition: WeatherCondition.fromJson(json["condition"]),
      high: JsonParser.parseInt(json["high"]),
      low: JsonParser.parseInt(json["low"]),
      humidity: JsonParser.parseInt(json["humidity"]),
      windDirection: JsonParser.parseInt(json["wind_direction"]),
      windSpeed: JsonParser.parseDouble(json["wind_speed"]),
      pop: JsonParser.parseInt(json["pop"]),
      uvIndex: JsonParser.parseDouble(json["uv_index"]),
    );
  }
  
  String get weekday {
    List<String> weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
    return weekdays[date.weekday % 7];
  }
}

class HourlyForecast {
  final DateTime time;
  final WeatherCondition condition;
  final int temp;
  final int feelsLike;
  final int humidity;
  final int windDirection;
  final double windSpeed;
  final int pop;
  
  HourlyForecast({
    required this.time,
    required this.condition,
    required this.temp,
    required this.feelsLike,
    required this.humidity,
    required this.windDirection,
    required this.windSpeed,
    required this.pop,
  });
  
  factory HourlyForecast.fromJson(Map<String, dynamic> json) {
    return HourlyForecast(
      time: JsonParser.parseDateTime(json["time"]) ?? DateTime.now(),
      condition: WeatherCondition.fromJson(json["condition"]),
      temp: JsonParser.parseInt(json["temp"]),
      feelsLike: JsonParser.parseInt(json["feels_like"]),
      humidity: JsonParser.parseInt(json["humidity"]),
      windDirection: JsonParser.parseInt(json["wind_direction"]),
      windSpeed: JsonParser.parseDouble(json["wind_speed"]),
      pop: JsonParser.parseInt(json["pop"]),
    );
  }
  
  String get hourString {
    return "${time.hour.toString().padLeft(2, "0")}:00";
  }
}

class WeatherResponse {
  final int code;
  final String message;
  final CurrentWeather? current;
  final List<DailyForecast>? daily;
  final List<HourlyForecast>? hourly;
  final AirQuality? airQuality;
  
  WeatherResponse({
    required this.code,
    required this.message,
    this.current,
    this.daily,
    this.hourly,
    this.airQuality,
  });
  
  factory WeatherResponse.fromJson(Map<String, dynamic> json) {
    List<dynamic>? dailyList = json["daily"] as List?;
    List<dynamic>? hourlyList = json["hourly"] as List?;
    
    return WeatherResponse(
      code: JsonParser.parseInt(json["code"]),
      message: JsonParser.parseString(json["message"]),
      current: json["current"] != null 
          ? CurrentWeather.fromJson(json["current"]) 
          : null,
      daily: dailyList?.map((e) => DailyForecast.fromJson(e)).toList(),
      hourly: hourlyList?.map((e) => HourlyForecast.fromJson(e)).toList(),
      airQuality: json["air_quality"] != null 
          ? AirQuality.fromJson(json["air_quality"]) 
          : null,
    );
  }
  
  bool get isSuccess => code == 200;
}

10. 数据模型设计最佳实践总结

10.1 设计流程

  1. 分析JSON结构:仔细分析API返回的JSON数据结构
  2. 划分实体:将JSON结构划分为多个实体类
  3. 定义字段:为每个实体定义字段,使用final确保不可变性
  4. 实现fromJson:实现从JSON到对象的转换
  5. 实现toJson:实现从对象到JSON的转换
  6. 添加扩展方法:使用扩展方法添加额外功能
  7. 编写测试:为数据模型编写单元测试

10.2 命名规范

  • 类名使用PascalCase
  • 字段名使用camelCase
  • JSON键名使用snake_case
  • 使用@JsonKey注解映射不同的命名风格

10.3 工具选择

场景 推荐工具
简单项目 手动实现
中等项目 json_serializable
需要不可变模型 freezed
需要极致性能 built_value

10.4 常见错误及解决方案

错误类型 原因 解决方案
字段命名不匹配 JSON键名与Dart字段名不同 使用@JsonKey注解
类型转换错误 JSON值类型与Dart字段类型不匹配 使用安全解析函数
空值错误 访问了可能为null的字段 使用空值检查和默认值
嵌套对象错误 嵌套对象为null 在访问前检查嵌套对象
数组处理错误 JSON数组格式不正确 添加类型检查和错误处理

11. 总结

良好的数据模型设计是高效JSON解析的基础。本章介绍了:

  1. 数据模型设计原则:单一职责、不可变性、类型安全、可扩展性、错误处理
  2. 标准数据模型设计:包含fromJson和toJson方法的标准模型
  3. 使用freezed实现不可变模型:代码生成方式实现不可变数据模型
  4. 错误安全的模型设计:安全解析函数和数据验证
  5. 模型扩展方法:使用扩展方法添加额外功能
  6. 泛型数据模型:创建通用的数据模型
  7. 数据模型版本管理:处理API版本迭代
  8. 数据模型测试:单元测试和属性测试

通过掌握本章内容,读者可以在天气查询应用及其他Flutter项目中设计出高质量、可维护的数据模型,为应用的稳定运行打下坚实基础。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐