freezedでネスト構造のAPIレスポンスをパースするときに行ったことを共有します。
前提
以下、こちらのOpenWeatherAPIの例で書いてます。
以下でAPIを叩くと
https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=${appId}
こんなレスポンスがもらえます。
{
"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
}
このレスポンスから、描画に必要な情報だけを取り出します。
こちらの記事をベースにしていたので、参考にしているので、View層で描画に必要な情報は以下になります。
今回は以下のWeatherクラスを作りました。
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)ようになります!
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」が取り出せます。
WeatherApiResponse.fromJson(json.decode(response.body)
実行結果
呼び出しコードは別途載せますが、無事にWeatherApiResponseにデータを格納することができました。
この結果に対して、freezedの抽象クラスに用意したtoEntityでWeatherが取り出せます。
WeatherApi.response(
weather: [WeatherInfo(
main: Clouds,
description: broken clouds,
icon: 04d)],
main: MainInfo(
temperature: 290.68,
pressure: 1014,
humidity: 73),
name: Tokyo)
呼び出し時のコード(参考)
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();
}
}