【Flutter】freezedでネストさせるとき

freezedでネスト構造のAPIレスポンスをパースするときに行ったことを共有します。

## 前提
以下、[こちらのOpenWeatherAPI](https://openweathermap.org/current)の例で書いてます。

以下でAPIを叩くと
```
https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=${appId}
```

こんなレスポンスがもらえます。
```json
{
    "coord": {
        "lon": 139.6917,
        "lat": 35.6895
    },
    "weather": [
        {
            "id": 800,
            "main": "Clear",
            "description": "clear sky",
            "icon": "01n"
        }
    ],
    "base": "stations",
    "main": {
        "temp": 287.65,
        "feels_like": 287.3,
        "temp_min": 285.4,
        "temp_max": 288.51,
        "pressure": 1017,
        "humidity": 82
    },
    "visibility": 10000,
    "wind": {
        "speed": 6.69,
        "deg": 200
    },
    "clouds": {
        "all": 0
    },
    "dt": 1648659355,
    "sys": {
        "type": 2,
        "id": 2038398,
        "country": "JP",
        "sunrise": 1648672190,
        "sunset": 1648717259
    },
    "timezone": 32400,
    "id": 1850144,
    "name": "Tokyo",
    "cod": 200
}
```

このレスポンスから、描画に必要な情報だけを取り出します。
[こちらの記事](https://betterprogramming.pub/flutter-clean-architecture-test-driven-development-practical-guide-445f388e8604)をベースにしていたので、参考にしているので、View層で描画に必要な情報は以下になります。

今回は以下のWeatherクラスを作りました。
```dart
import 'package:equatable/equatable.dart';

class Weather extends Equatable {
  const Weather({
    required this.cityName,
    required this.main,
    required this.description,
    required this.iconCode,
    required this.temperature,
    required this.pressure,
    required this.humidity,
  });

  final String cityName;
  final String main;
  final String description;
  final String iconCode;
  final double temperature;
  final int pressure;
  final int humidity;

  @override
  List<Object?> get props => [
    cityName,
    main,
    description,
    iconCode,
    temperature,
    pressure,
    humidity,
  ];
}
```

## freezedでのネストの構造（本題）
ネスト構造を持たせる場合は、「@JsonSerializable(explicitToJson: true)」の設定が必要でした。
https://github.com/rrousselGit/freezed/issues/86

なお以下には、Entityとして中身を取り出すメソッド（toEntity）も定義しています。freezedで独自メソッドを作る場合は、元のabstract classにimplementsするようにし、private constructorを追加します。

これで、以下のfreezedクラスを使って、レスポンス→WeatherApiResponseの形にパースを行い（fromJson）、WeatherApiResponseからWeatherの情報が取り出せる（toEntity）ようになります！

```dart
import 'package:hogehoge/entities/models/weather.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'weather_api.freezed.dart';
part 'weather_api.g.dart';

@freezed
abstract class WeatherApi with _$WeatherApi {
  @JsonSerializable(explicitToJson: true)
　　　　const WeatherApi._();
  const factory WeatherApi.request({
    required List<WeatherInfo>? weather,
    required MainInfo? main,
    required String? name,
  }) = WeatherApiRequest;
  const factory WeatherApi.response({
    required List<WeatherInfo>? weather,
    required MainInfo? main,
    required String? name,
  }) = WeatherApiResponse;

  factory WeatherApi.fromJson(Map<String, dynamic> json) => _$WeatherApiFromJson(json);
  Weather toEntity() => Weather(
      cityName: name ?? "",
      main: weather?[0].main ?? "",
      description: weather?[0].description ?? "",
      iconCode: weather?[0].icon ?? "",
      temperature: main?.temperature ?? 0,
      pressure: main?.pressure ?? 0,
      humidity: main?.humidity ?? 0,
  );
}

@freezed
abstract class WeatherInfo with _$WeatherInfo {
  @JsonSerializable(explicitToJson: true)
  const factory WeatherInfo({
    required String? main,
    required String? description,
    required String? icon,
  }) = _WeatherInfo;
  factory WeatherInfo.fromJson(Map<String, dynamic> json) => _$WeatherInfoFromJson(json);

}

@freezed
abstract class MainInfo with _$MainInfo {
  const factory MainInfo({
    @required @JsonKey(name: 'temp_max') double? temperature,
    required int? pressure,
    required int? humidity,
  }) = _MainInfo;
  factory MainInfo.fromJson(Map<String, dynamic> json) => _$MainInfoFromJson(json);
}
```

こちらを、APIを叩いた時のレスポンスに対して、以下実行することで「WeatherApiResponse」が取り出せます。
```dart
WeatherApiResponse.fromJson(json.decode(response.body)
```

## 実行結果
呼び出しコードは別途載せますが、無事にWeatherApiResponseにデータを格納することができました。
この結果に対して、freezedの抽象クラスに用意したtoEntityでWeatherが取り出せます。

```dart
WeatherApi.response(
    weather: [WeatherInfo(
        main: Clouds, 
        description: broken clouds, 
        icon: 04d)], 
    main: MainInfo(
        temperature: 290.68, 
        pressure: 1014, 
        humidity: 73), 
    name: Tokyo)
```

## 呼び出し時のコード（参考）
```dart
  Future<Either<ErrorApi, WeatherApiResponse>> getCurrentWeather(String cityName) async {
    http.Client client = http.Client();

    // クエリパラメータを定義します。
    final queryParameters = {
      'q': cityName,
      'appid': apiKey,
    };

    try {
      // クエリパラメータがある場合は、Urihttpを使うしかないようです
      final uri = Uri.https(baseAuth, "/data/2.5/weather", queryParameters);

      // final response = await client.get(uri, headers: headers);
      final response = await client.get(uri);
      if (200 <= response.statusCode && response.statusCode <= 300) {
        return Right(WeatherApiResponse.fromJson(json.decode(response.body)));
      } else {
        return Left(ErrorApi.fromJson(json.decode(response.body)));
      }
    } on SocketException catch (e){
      throw const SocketException('No Internet connection');
    } on Exception catch (e) {
      throw OtherException(e.toString());
    } finally {
      client.close();
    }
  }
```